This commit is contained in:
Sean Yesmunt 2019-09-27 14:56:15 -04:00
parent 4c014e3147
commit ecf5e52dd4
47 changed files with 462 additions and 453 deletions

View file

@ -24,6 +24,7 @@ module.name_mapper='^app\(.*\)$' -> '<PROJECT_ROOT>/src/ui/app\1'
module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/src/ui/native\1' module.name_mapper='^native\(.*\)$' -> '<PROJECT_ROOT>/src/ui/native\1'
module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/src/ui/analytics\1' module.name_mapper='^analytics\(.*\)$' -> '<PROJECT_ROOT>/src/ui/analytics\1'
module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/src/ui/i18n\1' module.name_mapper='^i18n\(.*\)$' -> '<PROJECT_ROOT>/src/ui/i18n\1'
module.name_mapper='^effects\(.*\)$' -> '<PROJECT_ROOT>/src/ui/effects\1'
module.name_mapper='^config\(.*\)$' -> '<PROJECT_ROOT>/config\1' module.name_mapper='^config\(.*\)$' -> '<PROJECT_ROOT>/config\1'

View file

@ -128,7 +128,7 @@
"husky": "^0.14.3", "husky": "^0.14.3",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#d44cd9ca56dee784dba42c0cc13061ae75cbd46c", "lbry-redux": "lbryio/lbry-redux#42bf926138872d14523be7191694309be4f37605",
"lbryinc": "lbryio/lbryinc#368040d64658cf2a4b8a7a6725ec1787329ce65d", "lbryinc": "lbryio/lbryinc#368040d64658cf2a4b8a7a6725ec1787329ce65d",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -11,8 +11,7 @@ import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
import FileViewer from 'component/fileViewer'; import FileViewer from 'component/fileViewer';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import usePrevious from 'util/use-previous'; import usePrevious from 'effects/use-previous';
import SyncBackgroundManager from 'component/syncBackgroundManager';
import Button from 'component/button'; import Button from 'component/button';
export const MAIN_WRAPPER_CLASS = 'main-wrapper'; export const MAIN_WRAPPER_CLASS = 'main-wrapper';
@ -121,7 +120,6 @@ function App(props: Props) {
<Router /> <Router />
<ModalRouter /> <ModalRouter />
<FileViewer pageUri={uri} /> <FileViewer pageUri={uri} />
<SyncBackgroundManager />
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
{showUpgradeButton && ( {showUpgradeButton && (

View file

@ -3,7 +3,7 @@ import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import useHover from 'util/use-hover'; import useHover from 'effects/use-hover';
type Props = { type Props = {
permanentUrl: ?string, permanentUrl: ?string,

View file

@ -64,7 +64,7 @@ function ChannelContent(props: Props) {
</div> </div>
)} )}
{!channelIsMine && <HiddenNsfwClaims className="card__subtitle" uri={uri} />} {!channelIsMine && <HiddenNsfwClaims uri={uri} />}
{hasContent && !channelIsBlocked && !channelIsBlackListed && ( {hasContent && !channelIsBlocked && !channelIsBlackListed && (
<ClaimList header={false} uris={claimsInChannel.map(claim => claim && claim.canonical_url)} /> <ClaimList header={false} uris={claimsInChannel.map(claim => claim && claim.canonical_url)} />

View file

@ -182,7 +182,7 @@ function ChannelForm(props: Props) {
onChange={text => setParams({ ...params, description: text })} onChange={text => setParams({ ...params, description: text })}
/> />
<TagSelect <TagSelect
title={false} title={__('Add Tags')}
suggestMature suggestMature
help={__('The better your tags are, the easier it will be for people to discover your channel.')} help={__('The better your tags are, the easier it will be for people to discover your channel.')}
empty={__('No tags added')} empty={__('No tags added')}

View file

@ -6,7 +6,7 @@ import classnames from 'classnames';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
const SORT_NEW = 'new'; const SORT_NEW = 'new';
const SORT_OLD = 'old'; const SORT_OLD = 'old';

View file

@ -187,9 +187,9 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
<div className="claim-preview-title"> <div className="claim-preview-title">
{claim ? <TruncatedText text={title || claim.name} lines={1} /> : <span>{__('Nothing here')}</span>} {claim ? <TruncatedText text={title || claim.name} lines={1} /> : <span>{__('Nothing here')}</span>}
</div> </div>
{actions !== undefined {!hideActions && actions !== undefined ? (
? actions actions
: !hideActions && ( ) : (
<div className="card__actions--inline"> <div className="card__actions--inline">
{isChannel && !channelIsBlocked && !claimIsMine && ( {isChannel && !channelIsBlocked && !claimIsMine && (
<SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} /> <SubscribeButton uri={uri.startsWith('lbry://') ? uri : `lbry://${uri}`} />

View file

@ -5,7 +5,7 @@ import { FormField, Form } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import ChannelSection from 'component/selectChannel'; import ChannelSection from 'component/selectChannel';
import UnsupportedOnWeb from 'component/common/unsupported-on-web'; import UnsupportedOnWeb from 'component/common/unsupported-on-web';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
type Props = { type Props = {
uri: string, uri: string,

View file

@ -5,7 +5,7 @@ import classnames from 'classnames';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
type Props = { type Props = {
title: string | Node, title?: string | Node,
subtitle?: string | Node, subtitle?: string | Node,
body?: string | Node, body?: string | Node,
actions?: string | Node, actions?: string | Node,
@ -16,6 +16,7 @@ export default function Card(props: Props) {
const { title, subtitle, body, actions, icon } = props; const { title, subtitle, body, actions, icon } = props;
return ( return (
<section className={classnames('card')}> <section className={classnames('card')}>
{title && (
<div className="card__header"> <div className="card__header">
<div className="section__flex"> <div className="section__flex">
{icon && <Icon sectionIcon icon={icon} />} {icon && <Icon sectionIcon icon={icon} />}
@ -25,6 +26,7 @@ export default function Card(props: Props) {
</div> </div>
</div> </div>
</div> </div>
)}
{body && <div className={classnames('card__body', { 'card__body--with-icon': icon })}>{body}</div>} {body && <div className={classnames('card__body', { 'card__body--with-icon': icon })}>{body}</div>}
{actions && ( {actions && (

View file

@ -6,8 +6,8 @@ import classnames from 'classnames';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import FileRender from 'component/fileRender'; import FileRender from 'component/fileRender';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import usePrevious from 'util/use-previous'; import usePrevious from 'effects/use-previous';
import { FILE_WRAPPER_CLASS } from 'page/file/view'; import { FILE_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
@ -86,7 +86,7 @@ export default function FileViewer(props: Props) {
function handleResize() { function handleResize() {
const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`); const element = document.querySelector(`.${FILE_WRAPPER_CLASS}`);
if (!element) { if (!element) {
console.error("Can't find file viewer wrapper to attach to the inline viewer to"); console.error("Can't find file viewer wrapper to attach to the inline viewer to"); // eslint-disable-line
return; return;
} }

View file

@ -1,7 +1,6 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import classnames from 'classnames';
type Props = { type Props = {
numberOfNsfwClaims: number, numberOfNsfwClaims: number,
@ -10,12 +9,12 @@ type Props = {
}; };
export default (props: Props) => { export default (props: Props) => {
const { numberOfNsfwClaims, obscureNsfw, className } = props; const { numberOfNsfwClaims, obscureNsfw } = props;
return ( return (
obscureNsfw && obscureNsfw &&
Boolean(numberOfNsfwClaims) && ( Boolean(numberOfNsfwClaims) && (
<div className={classnames('card--section', className || 'help')}> <div className="section--padded section__subtitle">
{numberOfNsfwClaims} {numberOfNsfwClaims > 1 ? __('files') : __('file')} {__('hidden due to your')}{' '} {numberOfNsfwClaims} {numberOfNsfwClaims > 1 ? __('files') : __('file')} {__('hidden due to your')}{' '}
<Button button="link" navigate="/$/settings" label={__('content viewing preferences')} />. <Button button="link" navigate="/$/settings" label={__('content viewing preferences')} />.
</div> </div>

View file

@ -1,10 +1,11 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import LicenseType from './license-type'; import LicenseType from './license-type';
import Card from 'component/common/card';
type Props = { type Props = {
language: ?string, language: ?string,
@ -25,7 +26,9 @@ function PublishAdvanced(props: Props) {
} }
return ( return (
<section className="card card--section"> <Card
actions={
<React.Fragment>
{!hideSection && ( {!hideSection && (
<div className={classnames({ 'card--disabled': !name })}> <div className={classnames({ 'card--disabled': !name })}>
<FormField <FormField
@ -83,9 +86,15 @@ function PublishAdvanced(props: Props) {
)} )}
<div className="card__actions"> <div className="card__actions">
<Button label={hideSection ? __('Additional Options') : __('Hide')} button="link" onClick={toggleHideSection} /> <Button
label={hideSection ? __('Additional Options') : __('Hide')}
button="link"
onClick={toggleHideSection}
/>
</div> </div>
</section> </React.Fragment>
}
/>
); );
} }

View file

@ -1,9 +1,10 @@
// @flow // @flow
import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import { regexInvalidURI } from 'lbry-redux'; import { regexInvalidURI } from 'lbry-redux';
import classnames from 'classnames';
import FileSelector from 'component/common/file-selector'; import FileSelector from 'component/common/file-selector';
import Button from 'component/button'; import Button from 'component/button';
import Card from 'component/common/card';
type Props = { type Props = {
name: ?string, name: ?string,
@ -11,10 +12,11 @@ type Props = {
isStillEditing: boolean, isStillEditing: boolean,
balance: number, balance: number,
updatePublishForm: ({}) => void, updatePublishForm: ({}) => void,
disabled: boolean,
}; };
function PublishFile(props: Props) { function PublishFile(props: Props) {
const { name, balance, filePath, isStillEditing, updatePublishForm } = props; const { name, balance, filePath, isStillEditing, updatePublishForm, disabled } = props;
function handleFileChange(filePath: string, fileName: string) { function handleFileChange(filePath: string, fileName: string) {
const publishFormParams: { filePath: string, name?: string } = { filePath }; const publishFormParams: { filePath: string, name?: string } = { filePath };
@ -28,15 +30,14 @@ function PublishFile(props: Props) {
} }
return ( return (
<section <Card
className={classnames('card card--section', { className={disabled ? 'card--disabled' : undefined}
'card--disabled': balance === 0, icon={ICONS.PUBLISH}
})} disabled={balance === 0}
> title={isStillEditing ? __('Edit') : __('Publish')}
<h2 className="card__title">{isStillEditing ? __('Edit') : __('Publish')}</h2> subtitle={__('You are currently editing a claim.')}
{isStillEditing && <p className="card__subtitle">{__('You are currently editing a claim.')}</p>} actions={
<React.Fragment>
<div className="card__content">
<FileSelector currentPath={filePath} onFileChosen={handleFileChange} /> <FileSelector currentPath={filePath} onFileChosen={handleFileChange} />
{!isStillEditing && ( {!isStillEditing && (
<p className="help"> <p className="help">
@ -49,8 +50,9 @@ function PublishFile(props: Props) {
{__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })} {__("If you don't choose a file, the file from your existing claim %name% will be used", { name: name })}
</p> </p>
)} )}
</div> </React.Fragment>
</section> }
/>
); );
} }

View file

@ -14,6 +14,7 @@ import PublishName from 'component/publishName';
import PublishAdditionalOptions from 'component/publishAdditionalOptions'; import PublishAdditionalOptions from 'component/publishAdditionalOptions';
import PublishFormErrors from 'component/publishFormErrors'; import PublishFormErrors from 'component/publishFormErrors';
import SelectThumbnail from 'component/selectThumbnail'; import SelectThumbnail from 'component/selectThumbnail';
import Card from 'component/common/card';
type Props = { type Props = {
tags: Array<Tag>, tags: Array<Tag>,
@ -127,13 +128,10 @@ function PublishForm(props: Props) {
<PublishFile /> <PublishFile />
<div className={classnames({ 'card--disabled': formDisabled })}> <div className={classnames({ 'card--disabled': formDisabled })}>
<PublishText disabled={formDisabled} /> <PublishText disabled={formDisabled} />
<div className="card card--section"> <Card actions={<SelectThumbnail />} />
{/* This should probably be PublishThumbnail */}
<SelectThumbnail />
</div>
<div className="card card--section">
<TagSelect <TagSelect
title={false} title={__('Add Tags')}
suggestMature suggestMature
help={__('The better your tags are, the easier it will be for people to discover your content.')} help={__('The better your tags are, the easier it will be for people to discover your content.')}
empty={__('No tags added')} empty={__('No tags added')}
@ -148,14 +146,18 @@ function PublishForm(props: Props) {
}} }}
tagsChosen={tags} tagsChosen={tags}
/> />
</div>
<section className="card card--section"> <Card
actions={
<React.Fragment>
<ChannelSection channel={channel} onChannelChange={channel => updatePublishForm({ channel })} /> <ChannelSection channel={channel} onChannelChange={channel => updatePublishForm({ channel })} />
<p className="help"> <p className="help">
{__('This is a username or handle that your content can be found under.')}{' '} {__('This is a username or handle that your content can be found under.')}{' '}
{__('Ex. @Marvel, @TheBeatles, @BooksByJoe')} {__('Ex. @Marvel, @TheBeatles, @BooksByJoe')}
</p> </p>
</section> </React.Fragment>
}
/>
<PublishName disabled={formDisabled} /> <PublishName disabled={formDisabled} />
<PublishPrice disabled={formDisabled} /> <PublishPrice disabled={formDisabled} />

View file

@ -5,6 +5,7 @@ import { isNameValid } from 'lbry-redux';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import NameHelpText from './name-help-text'; import NameHelpText from './name-help-text';
import BidHelpText from './bid-help-text'; import BidHelpText from './bid-help-text';
import Card from 'component/common/card';
type Props = { type Props = {
name: string, name: string,
@ -73,7 +74,9 @@ function PublishName(props: Props) {
}, [bid, previousBidAmount, balance]); }, [bid, previousBidAmount, balance]);
return ( return (
<section className="card card--section"> <Card
actions={
<React.Fragment>
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix"> <fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section> <fieldset-section>
<label>{__('Name')}</label> <label>{__('Name')}</label>
@ -111,10 +114,16 @@ function PublishName(props: Props) {
disabled={!name} disabled={!name}
onChange={event => updatePublishForm({ bid: parseFloat(event.target.value) })} onChange={event => updatePublishForm({ bid: parseFloat(event.target.value) })}
helper={ helper={
<BidHelpText uri={uri} amountNeededForTakeover={amountNeededForTakeover} isResolvingUri={isResolvingUri} /> <BidHelpText
uri={uri}
amountNeededForTakeover={amountNeededForTakeover}
isResolvingUri={isResolvingUri}
/>
}
/>
</React.Fragment>
} }
/> />
</section>
); );
} }

View file

@ -1,6 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { FormField, FormFieldPrice } from 'component/common/form'; import { FormField, FormFieldPrice } from 'component/common/form';
import Card from 'component/common/card';
type Props = { type Props = {
contentIsFree: boolean, contentIsFree: boolean,
@ -13,7 +14,9 @@ function PublishPrice(props: Props) {
const { contentIsFree, fee, updatePublishForm, disabled } = props; const { contentIsFree, fee, updatePublishForm, disabled } = props;
return ( return (
<section className="card card--section"> <Card
actions={
<React.Fragment>
<FormField <FormField
type="radio" type="radio"
name="content_free" name="content_free"
@ -46,7 +49,9 @@ function PublishPrice(props: Props) {
)} )}
</p> </p>
)} )}
</section> </React.Fragment>
}
/>
); );
} }

View file

@ -2,7 +2,8 @@
import React from 'react'; import React from 'react';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import Card from 'component/common/card';
type Props = { type Props = {
title: ?string, title: ?string,
@ -19,7 +20,9 @@ function PublishText(props: Props) {
} }
return ( return (
<section className="card card--section"> <Card
actions={
<React.Fragment>
<FormField <FormField
type="text" type="text"
name="content_title" name="content_title"
@ -40,9 +43,15 @@ function PublishText(props: Props) {
onChange={value => updatePublishForm({ description: advancedEditor ? value : value.target.value })} onChange={value => updatePublishForm({ description: advancedEditor ? value : value.target.value })}
/> />
<div className="card__actions"> <div className="card__actions">
<Button button="link" onClick={toggleMarkdown} label={advancedEditor ? 'Simple Editor' : 'Advanced Editor'} /> <Button
button="link"
onClick={toggleMarkdown}
label={advancedEditor ? 'Simple Editor' : 'Advanced Editor'}
/>
</div> </div>
</section> </React.Fragment>
}
/>
); );
} }

View file

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import TotalBackground from './total-background.png'; import TotalBackground from './total-background.png';
import useTween from 'util/use-tween'; import useTween from 'effects/use-tween';
type Props = { type Props = {
rewards: Array<Reward>, rewards: Array<Reward>,

View file

@ -69,7 +69,8 @@ function SideBar(props: Props) {
className="navigation-link" className="navigation-link"
activeClass="navigation-link--active" activeClass="navigation-link--active"
/> />
<ul className="navigation-links tags--vertical"> <section className="navigation-links__inline">
<ul className="navigation-links--small tags--vertical">
{followedTags.map(({ name }, key) => ( {followedTags.map(({ name }, key) => (
<li className="navigation-link__wrapper" key={name}> <li className="navigation-link__wrapper" key={name}>
<Tag navigate={`/$/tags?t${name}`} name={name} /> <Tag navigate={`/$/tags?t${name}`} name={name} />
@ -88,6 +89,7 @@ function SideBar(props: Props) {
</li> </li>
))} ))}
</ul> </ul>
</section>
</nav> </nav>
</StickyBox> </StickyBox>
); );

View file

@ -124,11 +124,6 @@ export default class SplashScreen extends React.PureComponent<Props, State> {
clearTimeout(this.timeout); clearTimeout(this.timeout);
} }
//
//
// Try to unlock by default here
//
//
// Make sure there isn't another active modal (like INCOMPATIBLE_DAEMON) // Make sure there isn't another active modal (like INCOMPATIBLE_DAEMON)
if (launchedModal === false && !modal) { if (launchedModal === false && !modal) {
this.setState({ launchedModal: true }, () => notifyUnlockWallet()); this.setState({ launchedModal: true }, () => notifyUnlockWallet());
@ -153,10 +148,10 @@ export default class SplashScreen extends React.PureComponent<Props, State> {
}); });
} }
} else if (wallet && wallet.blocks_behind > 0) { } else if (wallet && wallet.blocks_behind > 0) {
const format = wallet.blocks_behind === 1 ? '%s block behind' : '%s blocks behind'; const amountBehind = wallet.blocks_behind === 1 ? '%amountBehind% block behind' : '%amountBehind% blocks behind';
this.setState({ this.setState({
message: __('Blockchain Sync'), message: __('Blockchain Sync'),
details: `${__('Catching up...')} (${__(format, wallet.blocks_behind)})`, details: `${__('Catching up...')} (${__(amountBehind, { amountBehind: wallet.blocks_behind })})`,
}); });
} else if ( } else if (
wallet && wallet &&

View file

@ -4,7 +4,7 @@ import * as ICONS from 'constants/icons';
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import Button from 'component/button'; import Button from 'component/button';
import useHover from 'util/use-hover'; import useHover from 'effects/use-hover';
type SubscriptionArgs = { type SubscriptionArgs = {
channelName: string, channelName: string,

View file

@ -1,24 +0,0 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux';
import { doGetSync, selectSyncHash, selectUserVerifiedEmail } from 'lbryinc';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import WalletSecurityAndSync from './view';
const select = state => ({
balance: selectBalance(state),
verifiedEmail: selectUserVerifiedEmail(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
hasSyncHash: selectSyncHash(state),
});
const perform = dispatch => ({
getSync: password => dispatch(doGetSync(password)),
setSync: value => dispatch(doSetClientSetting(SETTINGS.ENABLE_SYNC, value)),
});
export default connect(
select,
perform
)(WalletSecurityAndSync);

View file

@ -1,22 +0,0 @@
// @flow
import React from 'react';
type Props = {
syncEnabled: boolean,
verifiedEmail?: string,
getSync: () => void,
};
function SyncBackgroundManager(props: Props) {
const { syncEnabled, getSync, verifiedEmail } = props;
React.useEffect(() => {
if (syncEnabled && verifiedEmail) {
getSync();
}
}, [syncEnabled, verifiedEmail, getSync]);
return null;
}
export default SyncBackgroundManager;

View file

@ -4,7 +4,7 @@ import * as React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import Tag from 'component/tag'; import Tag from 'component/tag';
import TagsSearch from 'component/tagsSearch'; import TagsSearch from 'component/tagsSearch';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import analytics from 'analytics'; import analytics from 'analytics';
import Card from 'component/common/card'; import Card from 'component/common/card';
@ -73,7 +73,15 @@ export default function TagSelect(props: Props) {
)} )}
</React.Fragment> </React.Fragment>
} }
body={ subtitle={
help !== false && (
<span>
{help || __("The tags you follow will change what's trending for you.")}{' '}
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/trending" />.
</span>
)
}
actions={
<React.Fragment> <React.Fragment>
<TagsSearch <TagsSearch
onRemove={handleTagClick} onRemove={handleTagClick}
@ -81,12 +89,6 @@ export default function TagSelect(props: Props) {
suggestMature={suggestMature && !hasMatureTag} suggestMature={suggestMature && !hasMatureTag}
tagsPasssedIn={tagsToDisplay} tagsPasssedIn={tagsToDisplay}
/> />
{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>
)}
</React.Fragment> </React.Fragment>
} }
/> />

View file

@ -40,9 +40,7 @@ function UserEmail(props: Props) {
'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.' '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.'
)} )}
actions={ actions={
!isVerified ? ( isVerified ? (
<Button button="primary" label={__('Add Email')} navigate={`/$/${PAGES.AUTH}`} />
) : (
<FormField <FormField
type="text" type="text"
className="form-field--copyable" className="form-field--copyable"
@ -60,6 +58,8 @@ function UserEmail(props: Props) {
inputButton={<UserSignOutButton button="inverse" />} inputButton={<UserSignOutButton button="inverse" />}
value={email || ''} value={email || ''}
/> />
) : (
<Button button="primary" label={__('Add Email')} navigate={`/$/${PAGES.AUTH}`} />
) )
} }
/> />

View file

@ -30,6 +30,7 @@ function UserEmailNew(props: Props) {
} }
React.useEffect(() => { React.useEffect(() => {
// Sync currently doesn't work for wallets with balances
if (syncEnabled && balance) { if (syncEnabled && balance) {
setSync(false); setSync(false);
} }
@ -56,7 +57,9 @@ function UserEmailNew(props: Props) {
name="sync_checkbox" name="sync_checkbox"
label={__('Sync your balance between devices')} label={__('Sync your balance between devices')}
helper={ helper={
balance > 0 ? __('This is only available for empty wallets') : __('Maybe some more text about something') balance > 0
? __("This is only available if you don't have a balance")
: __('Maybe some more text about something')
} }
checked={syncEnabled} checked={syncEnabled}
onChange={() => setSync(!syncEnabled)} onChange={() => setSync(!syncEnabled)}

View file

@ -33,9 +33,9 @@ const select = state => ({
youtubeChannels: selectYoutubeChannels(state), youtubeChannels: selectYoutubeChannels(state),
userFetchPending: selectUserIsPending(state), userFetchPending: selectUserIsPending(state),
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state), syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
syncIsPending: selectGetSyncIsPending(state), syncingWallet: selectGetSyncIsPending(state),
getSyncError: selectGetSyncErrorMessage(state), getSyncError: selectGetSyncErrorMessage(state),
syncHash: selectSyncHash(state), hasSynced: Boolean(selectSyncHash(state)),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -6,19 +6,11 @@ import UserEmailVerify from 'component/userEmailVerify';
import UserFirstChannel from 'component/userFirstChannel'; import UserFirstChannel from 'component/userFirstChannel';
import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view'; import { DEFAULT_BID_FOR_FIRST_CHANNEL } from 'component/userFirstChannel/view';
import { rewards as REWARDS } from 'lbryinc'; import { rewards as REWARDS } from 'lbryinc';
import usePrevious from 'util/use-previous';
import UserVerify from 'component/userVerify'; import UserVerify from 'component/userVerify';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import YoutubeTransferWelcome from 'component/youtubeTransferWelcome'; import YoutubeTransferWelcome from 'component/youtubeTransferWelcome';
import SyncPassword from 'component/syncPassword'; import SyncPassword from 'component/syncPassword';
import useFetched from 'effects/use-fetched';
/*
- Brand new user
- Brand new user, not auto approved
- Second device (first time user), first device has a password + rewards not approved
- Second device (first time user), first device has a password + rewards approved
*/
type Props = { type Props = {
user: ?User, user: ?User,
@ -33,24 +25,12 @@ type Props = {
history: { replace: string => void }, history: { replace: string => void },
location: { search: string }, location: { search: string },
youtubeChannels: Array<any>, youtubeChannels: Array<any>,
syncIsPending: boolean, syncEnabled: boolean,
hasSynced: boolean,
syncingWallet: boolean,
getSyncError: ?string, getSyncError: ?string,
hasSyncedSuccessfully: boolean,
}; };
function useFetched(fetching) {
const wasFetching = usePrevious(fetching);
const [fetched, setFetched] = React.useState(false);
React.useEffect(() => {
if (wasFetching && !fetching) {
setFetched(true);
}
}, [wasFetching, fetching, setFetched]);
return fetched;
}
function UserSignIn(props: Props) { function UserSignIn(props: Props) {
const { const {
emailToVerify, emailToVerify,
@ -65,9 +45,9 @@ function UserSignIn(props: Props) {
fetchUser, fetchUser,
youtubeChannels, youtubeChannels,
syncEnabled, syncEnabled,
syncIsPending, syncingWallet,
getSyncError, getSyncError,
syncHash, hasSynced,
fetchingChannels, fetchingChannels,
} = props; } = props;
const { search } = location; const { search } = location;
@ -76,52 +56,60 @@ function UserSignIn(props: Props) {
const hasVerifiedEmail = user && user.has_verified_email; const hasVerifiedEmail = user && user.has_verified_email;
const rewardsApproved = user && user.is_reward_approved; const rewardsApproved = user && user.is_reward_approved;
const hasFetchedReward = useFetched(claimingReward); const hasFetchedReward = useFetched(claimingReward);
// const hasFetchedSync = useFetched(syncIsPending);
// const hasTriedSyncForReal = syncEnabled && hasFetchedSync;
const channelCount = channels ? channels.length : 0; const channelCount = channels ? channels.length : 0;
const hasClaimedEmailAward = claimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_CONFIRM_EMAIL); const hasClaimedEmailAward = claimedRewards.some(reward => reward.reward_type === REWARDS.TYPE_CONFIRM_EMAIL);
const hasYoutubeChannels = youtubeChannels && youtubeChannels.length; const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
const hasTransferrableYoutubeChannels = hasYoutubeChannels && youtubeChannels.some(channel => channel.transferable); const hasTransferrableYoutubeChannels = hasYoutubeChannels && youtubeChannels.some(channel => channel.transferable);
const hasPendingYoutubeTransfer = const hasPendingYoutubeTransfer =
hasYoutubeChannels && youtubeChannels.some(channel => channel.transfer_state === 'pending_transfer'); hasYoutubeChannels && youtubeChannels.some(channel => channel.transfer_state === 'pending_transfer');
React.useEffect(() => { // Complexity warning
if ( // We can't just check if we are currently fetching something
hasVerifiedEmail && // We may want to keep a component rendered while something is being fetched, instead of replacing it with the large spinner
balance !== undefined && // The verbose variable names are an attempt to alleviate _some_ of the confusion from handling all edge cases that come from
!hasClaimedEmailAward && // reward claiming (plus the balance updating after), channel creation, account syncing, and youtube transfer
!hasFetchedReward && const canHijackSignInFlowWithSpinner = hasVerifiedEmail && balance === 0 && !getSyncError;
(!syncEnabled || (syncEnabled && syncHash)) const isCurrentlyFetchingSomething = fetchingChannels || claimingReward || syncingWallet;
) { const isWaitingForSomethingToFinish =
claimReward(); // If the user has claimed the email award, we need to wait until the balance updates sometime in the future
} !hasFetchedReward || (hasFetchedReward && hasClaimedEmailAward) || (syncEnabled && !hasSynced);
}, [hasVerifiedEmail, claimReward, balance, hasClaimedEmailAward, hasFetchedReward, syncEnabled, syncHash]);
// The possible screens for the sign in flow
const showEmail = !emailToVerify && !hasVerifiedEmail;
const showEmailVerification = emailToVerify && !hasVerifiedEmail;
const showUserVerification = hasVerifiedEmail && !rewardsApproved;
const showSyncPassword = syncEnabled && getSyncError && !hasSynced;
const showChannelCreation =
hasVerifiedEmail && balance && balance > DEFAULT_BID_FOR_FIRST_CHANNEL && channelCount === 0 && !hasYoutubeChannels;
const showYoutubeTransfer =
hasVerifiedEmail && hasYoutubeChannels && (hasTransferrableYoutubeChannels || hasPendingYoutubeTransfer);
const showLoadingSpinner =
canHijackSignInFlowWithSpinner && (isCurrentlyFetchingSomething || isWaitingForSomethingToFinish);
React.useEffect(() => { React.useEffect(() => {
fetchUser(); fetchUser();
}, [fetchUser]); }, [fetchUser]);
React.useEffect(() => {
// Don't claim the reward if sync is enabled until after a sync has been completed successfully
// If we do it before, we could end up trying to sync a wallet with a non-zero balance which will fail to sync
const delayForSync = syncEnabled && !hasSynced;
if (hasVerifiedEmail && !hasClaimedEmailAward && !hasFetchedReward && !delayForSync) {
claimReward();
}
}, [hasVerifiedEmail, claimReward, hasClaimedEmailAward, hasFetchedReward, syncEnabled, hasSynced]);
// Loop through this list from the end, until it finds a matching component
// If it never finds one, assume the user has completed every step and redirect them
const SIGN_IN_FLOW = [ const SIGN_IN_FLOW = [
!emailToVerify && !hasVerifiedEmail && <UserEmailNew />, showEmail && <UserEmailNew />,
emailToVerify && !hasVerifiedEmail && <UserEmailVerify />, showEmailVerification && <UserEmailVerify />,
hasVerifiedEmail && !rewardsApproved && <UserVerify />, showUserVerification && <UserVerify />,
getSyncError && !syncHash && <SyncPassword />, showSyncPassword && <SyncPassword />,
hasVerifiedEmail && balance > DEFAULT_BID_FOR_FIRST_CHANNEL && channelCount === 0 && !hasYoutubeChannels && ( showChannelCreation && <UserFirstChannel />,
<UserFirstChannel /> showYoutubeTransfer && <YoutubeTransferWelcome />,
), showLoadingSpinner && (
hasVerifiedEmail && hasYoutubeChannels && (hasTransferrableYoutubeChannels || hasPendingYoutubeTransfer) && (
<YoutubeTransferWelcome />
),
hasVerifiedEmail &&
balance === 0 &&
!getSyncError &&
(fetchingChannels ||
!hasFetchedReward ||
claimingReward ||
syncIsPending ||
(syncEnabled && !syncHash) ||
// Just claimed the email award, wait until the balance updates to move forward
(balance === 0 && hasFetchedReward && hasClaimedEmailAward)) && (
<div className="main--empty"> <div className="main--empty">
<Spinner /> <Spinner />
</div> </div>

View file

@ -13,25 +13,33 @@ type Props = {
videosImported: ?Array<number>, // [currentAmountImported, totalAmountToImport] videosImported: ?Array<number>, // [currentAmountImported, totalAmountToImport]
}; };
const LBRY_YT_URL = 'https://lbry.com/youtube/status/'; const NOT_TRANSFERRED = 'not_transferred';
const NOT_TRANSFERED = 'not_transferred';
const PENDING_TRANSFER = 'pending_transfer'; const PENDING_TRANSFER = 'pending_transfer';
const COMPLETED_TRANSFER = 'completed_transfer'; const COMPLETED_TRANSFER = 'completed_transfer';
export default function YoutubeTransferStatus(props: Props) { export default function YoutubeTransferStatus(props: Props) {
const { youtubeChannels, ytImportPending, claimChannels, videosImported, checkYoutubeTransfer, updateUser } = props; const { youtubeChannels, ytImportPending, claimChannels, videosImported, checkYoutubeTransfer, updateUser } = props;
const hasChannels = youtubeChannels && youtubeChannels.length; const hasChannels = youtubeChannels && youtubeChannels.length;
const transferEnabled = youtubeChannels && youtubeChannels.some(el => el.transferable === true);
const transferComplete =
youtubeChannels &&
youtubeChannels.some(({ transfer_state: transferState }) => transferState === COMPLETED_TRANSFER);
let youtubeUrls = let transferEnabled = false;
youtubeChannels && let transferStarted = false;
youtubeChannels.map( let transferComplete = false;
({ lbry_channel_name: channelName, channel_claim_id: claimId }) => `lbry://${channelName}#${claimId}` if (hasChannels) {
); for (var i = 0; i < youtubeChannels.length; i++) {
const { transfer_state: transferState, transferable } = youtubeChannels[i];
if (transferable) {
transferEnabled = true;
}
if (transferState === COMPLETED_TRANSFER) {
transferComplete = true;
}
if (transferState === PENDING_TRANSFER) {
transferStarted = true;
}
}
}
let total; let total;
let complete; let complete;
@ -44,7 +52,7 @@ export default function YoutubeTransferStatus(props: Props) {
const { transferable, transfer_state: transferState, sync_status: syncStatus } = channel; const { transferable, transfer_state: transferState, sync_status: syncStatus } = channel;
if (!transferable) { if (!transferable) {
switch (transferState) { switch (transferState) {
case NOT_TRANSFERED: case NOT_TRANSFERRED:
return syncStatus[0].toUpperCase() + syncStatus.slice(1); return syncStatus[0].toUpperCase() + syncStatus.slice(1);
case PENDING_TRANSFER: case PENDING_TRANSFER:
return __('Transfer in progress'); return __('Transfer in progress');
@ -58,7 +66,7 @@ export default function YoutubeTransferStatus(props: Props) {
React.useEffect(() => { React.useEffect(() => {
// If a channel is transferrable, theres nothing to check // If a channel is transferrable, theres nothing to check
if (!transferComplete) { if (transferStarted && !transferComplete) {
checkYoutubeTransfer(); checkYoutubeTransfer();
let interval = setInterval(() => { let interval = setInterval(() => {
@ -70,30 +78,29 @@ export default function YoutubeTransferStatus(props: Props) {
clearInterval(interval); clearInterval(interval);
}; };
} }
}, [transferComplete, checkYoutubeTransfer, updateUser]); }, [transferComplete, transferStarted, checkYoutubeTransfer, updateUser]);
return ( return (
hasChannels && hasChannels &&
!transferComplete && ( !transferComplete && (
<div> <div>
<Card <Card
title={youtubeUrls.length > 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')} title={youtubeChannels.length > 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')}
subtitle={ subtitle={
<span> <span>
{__('Your videos are currently being transferred. There is nothing else for you to do.')}{' '} {transferStarted
<Button button="link" href={LBRY_YT_URL} label={__('Learn more')} />. ? __('Your videos are currently being transferred. There is nothing else for you to do.')
: __('Your videos are ready to be transferred.')}
</span> </span>
} }
body={ body={
<section> <section>
{youtubeUrls.map((url, index) => { {youtubeChannels.map((channel, index) => {
const channel = youtubeChannels[index]; const { lbry_channel_name: channelName, channel_claim_id: claimId } = channel;
const url = `lbry://${channelName}#${claimId}`;
const transferState = getMessage(channel); const transferState = getMessage(channel);
return ( return (
<div <div key={url} className="card--inline">
key={url}
style={{ border: '1px solid #ccc', borderRadius: 'var(--card-radius)', marginBottom: '1rem' }}
>
<ClaimPreview uri={url} actions={<span className="help">{transferState}</span>} properties={''} /> <ClaimPreview uri={url} actions={<span className="help">{transferState}</span>} properties={''} />
</div> </div>
); );

View file

@ -0,0 +1,17 @@
// @flow
import React from 'react';
import usePrevious from 'effects/use-previous';
// Returns true once a loading value has changed from false => true => false
export default function useFetched(fetching: boolean) {
const wasFetching = usePrevious(fetching);
const [fetched, setFetched] = React.useState(false);
React.useEffect(() => {
if (wasFetching && !fetching) {
setFetched(true);
}
}, [wasFetching, fetching, setFetched]);
return fetched;
}

View file

@ -3,7 +3,7 @@ import React from 'react';
import { Modal } from 'modal/modal'; import { Modal } from 'modal/modal';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import usePersistedState from 'util/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
type Props = { type Props = {
uri: string, uri: string,

View file

@ -16,7 +16,7 @@ function DiscoverPage(props: Props) {
return ( return (
<Page> <Page>
{email && <TagsSelect showClose title={__('Customize Your Homepage')} />} {(email || !IS_WEB) && <TagsSelect showClose title={__('Customize Your Homepage')} />}
<ClaimListDiscover <ClaimListDiscover
hideCustomization={IS_WEB && !email} hideCustomization={IS_WEB && !email}
personalView personalView

View file

@ -38,9 +38,7 @@ function FollowingPage(props: Props) {
return ( return (
<Page> <Page>
<div className="card card--section"> <TagsSelect showClose={false} title={__('Follow New Tags')} />
<TagsSelect showClose={false} title={__('Customize Your Tags')} />
</div>
<div className="card"> <div className="card">
<ClaimList <ClaimList
header={viewingSuggestedSubs ? __('Discover New Channels') : __('Channels You Follow')} header={viewingSuggestedSubs ? __('Discover New Channels') : __('Channels You Follow')}

View file

@ -3,7 +3,7 @@ import React, { useRef } from 'react';
import Page from 'component/page'; import Page from 'component/page';
import ClaimListDiscover from 'component/claimListDiscover'; import ClaimListDiscover from 'component/claimListDiscover';
import Button from 'component/button'; import Button from 'component/button';
import useHover from 'util/use-hover'; import useHover from 'effects/use-hover';
import analytics from 'analytics'; import analytics from 'analytics';
type Props = { type Props = {

View file

@ -16,6 +16,7 @@ import {
makeSelectClaimIsMine, makeSelectClaimIsMine,
doPopulateSharedUserState, doPopulateSharedUserState,
doFetchChannelListMine, doFetchChannelListMine,
getSync,
} from 'lbry-redux'; } from 'lbry-redux';
import Native from 'native'; import Native from 'native';
import { doFetchDaemonSettings } from 'redux/actions/settings'; import { doFetchDaemonSettings } from 'redux/actions/settings';
@ -454,6 +455,8 @@ export function doSignIn() {
dispatch(doBalanceSubscribe()); dispatch(doBalanceSubscribe());
dispatch(doCheckSubscriptionsInit()); dispatch(doCheckSubscriptionsInit());
dispatch(doFetchChannelListMine()); dispatch(doFetchChannelListMine());
dispatch(getSync());
// @endif // @endif
Lbryio.call('user_settings', 'get').then(settings => { Lbryio.call('user_settings', 'get').then(settings => {

View file

@ -55,15 +55,20 @@
margin-right: auto; margin-right: auto;
} }
// "cards" inside cards
.card--inline { .card--inline {
box-shadow: none; border: 1px solid $lbry-gray-1;
border-radius: none; border-radius: var(--card-radius);
margin-bottom: 0;
[data-mode='dark'] & {
border-color: var(--dm-color-03);
}
} }
.card--claim-preview-wrap { .card--claim-preview-wrap {
@extend .card; @extend .card;
margin: var(--spacing-xlarge) 0; margin: var(--spacing-xlarge) 0;
min-width: 35rem;
} }
.card--claim-preview-selected { .card--claim-preview-selected {
@ -241,7 +246,7 @@
.card__main-actions { .card__main-actions {
padding: var(--spacing-large); padding: var(--spacing-large);
background-color: rgba($lbry-blue-1, 0.1); background-color: var(--color-card-actions);
color: darken($lbry-gray-5, 15%); color: darken($lbry-gray-5, 15%);
font-size: var(--font-body); font-size: var(--font-body);

View file

@ -1,6 +1,12 @@
$border-color: rgba($lbry-teal-5, 0.1); $border-color: rgba($lbry-teal-5, 0.1);
$border-color--dark: var(--dm-color-04); $border-color--dark: var(--dm-color-04);
.claim-list {
.claim-preview {
border-top: 1px solid $border-color;
}
}
.claim-list__header { .claim-list__header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -85,6 +91,7 @@ $border-color--dark: var(--dm-color-04);
} }
.claim-preview { .claim-preview {
flex: 1;
display: flex; display: flex;
position: relative; position: relative;
overflow: visible; overflow: visible;
@ -99,14 +106,6 @@ $border-color--dark: var(--dm-color-04);
flex-shrink: 0; flex-shrink: 0;
margin-right: var(--spacing-medium); margin-right: var(--spacing-medium);
} }
}
.claim-preview {
border-top: 1px solid $border-color;
&:only-of-type {
border: none;
}
[data-mode='dark'] & { [data-mode='dark'] & {
color: $lbry-white; color: $lbry-white;
@ -114,14 +113,6 @@ $border-color--dark: var(--dm-color-04);
} }
} }
.claim-preview--injected + .claim-preview {
border-top: 1px solid $border-color;
[data-mode='dark'] & {
border-color: $border-color--dark;
}
}
.claim-preview--large { .claim-preview--large {
border: none; border: none;
padding: 0; padding: 0;

View file

@ -1,5 +1,5 @@
.icon__wrapper { .icon__wrapper {
@extend .card__subtitle; background-color: var(--color-card-actions);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View file

@ -66,10 +66,13 @@
} }
.main--contained { .main--contained {
max-width: 40rem;
min-width: 25rem;
margin: auto; margin: auto;
margin-top: 5rem; margin-top: 5rem;
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 40rem;
text-align: left;
} }
.main--full-width { .main--full-width {

View file

@ -39,6 +39,10 @@
font-size: var(--font-multiplier-small); font-size: var(--font-multiplier-small);
} }
.navigation-links__inline {
margin-left: 1.7rem;
}
.navigation-link__wrapper { .navigation-link__wrapper {
margin: var(--spacing-miniscule) 0; margin: var(--spacing-miniscule) 0;
} }

View file

@ -40,6 +40,7 @@ $large-breakpoint: 1921px;
// Color // Color
--color-background: #f7f7f7; --color-background: #f7f7f7;
--color-background--splash: #270f34; --color-background--splash: #270f34;
--color-card-actions: #f7fbfe;
// Dark Mode // Dark Mode
--dm-color-01: #ddd; --dm-color-01: #ddd;

View file

@ -6850,9 +6850,9 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#d44cd9ca56dee784dba42c0cc13061ae75cbd46c: lbry-redux@lbryio/lbry-redux#42bf926138872d14523be7191694309be4f37605:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/d44cd9ca56dee784dba42c0cc13061ae75cbd46c" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/42bf926138872d14523be7191694309be4f37605"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"