Merge pull request #2210 from lbryio/first-run

Less intrusive first run experience
This commit is contained in:
Sean Yesmunt 2019-01-25 14:56:44 -05:00 committed by GitHub
commit 8c0608bc74
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 668 additions and 195 deletions

View file

@ -67,6 +67,7 @@
"react-feather": "^1.0.8",
"react-modal": "^3.1.7",
"react-paginate": "^5.2.1",
"react-pose": "^4.0.5",
"react-redux": "^5.0.3",
"react-simplemde-editor": "^3.6.16",
"react-toggle": "^4.0.2",

View file

@ -7,8 +7,10 @@ import {
doError,
} from 'lbry-redux';
import { doRecordScroll } from 'redux/actions/navigation';
import { doToggleEnhancedLayout } from 'redux/actions/app';
import { selectUser } from 'lbryinc';
import { selectThemePath } from 'redux/selectors/settings';
import { selectEnhancedLayout } from 'redux/selectors/app';
import App from './view';
const select = state => ({
@ -17,12 +19,14 @@ const select = state => ({
currentStackIndex: selectHistoryIndex(state),
currentPageAttributes: selectActiveHistoryEntry(state),
theme: selectThemePath(state),
enhancedLayout: selectEnhancedLayout(state),
});
const perform = dispatch => ({
alertError: errorList => dispatch(doError(errorList)),
recordScroll: scrollPosition => dispatch(doRecordScroll(scrollPosition)),
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
toggleEnhancedLayout: () => dispatch(doToggleEnhancedLayout()),
});
export default connect(

View file

@ -6,7 +6,9 @@ import ReactModal from 'react-modal';
import throttle from 'util/throttle';
import SideBar from 'component/sideBar';
import Header from 'component/header';
import { openContextMenu } from '../../util/context-menu';
import { openContextMenu } from 'util/context-menu';
import EnhancedLayoutListener from 'util/enhanced-layout';
import Native from 'native';
const TWO_POINT_FIVE_MINUTES = 1000 * 60 * 2.5;
@ -18,6 +20,8 @@ type Props = {
pageTitle: ?string,
theme: string,
updateBlockHeight: () => void,
toggleEnhancedLayout: () => void,
enhancedLayout: boolean,
};
class App extends React.PureComponent<Props> {
@ -41,17 +45,18 @@ class App extends React.PureComponent<Props> {
}
componentDidMount() {
const { updateBlockHeight } = this.props;
const { updateBlockHeight, toggleEnhancedLayout } = this.props;
const mainContent = document.getElementById('content');
this.mainContent = mainContent;
if (this.mainContent) {
this.mainContent.addEventListener('scroll', throttle(this.scrollListener, 750));
}
ReactModal.setAppElement('#window'); // fuck this
this.enhance = new EnhancedLayoutListener(() => toggleEnhancedLayout());
updateBlockHeight();
setInterval(() => {
updateBlockHeight();
@ -81,6 +86,8 @@ class App extends React.PureComponent<Props> {
if (this.mainContent) {
this.mainContent.removeEventListener('scroll', this.scrollListener);
}
this.enhance = null;
}
setTitleFromProps = (title: ?string) => {
@ -96,12 +103,22 @@ class App extends React.PureComponent<Props> {
}
mainContent: ?HTMLElement;
enhance: ?any;
render() {
const { enhancedLayout } = this.props;
return (
<div id="window" onContextMenu={e => openContextMenu(e)}>
<Header />
<main className="page">
{enhancedLayout && (
<img
alt="Friendly gerbil"
className="yrbl--enhanced"
src={Native.imagePath('gerbil-happy.png')}
/>
)}
<SideBar />
<div className="content" id="content">
<Router />

View file

@ -0,0 +1,29 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { selectEmailToVerify, selectUser } from 'lbryinc';
import FirstRunEmailCollection from './view';
const select = state => ({
emailCollectionAcknowledged: makeSelectClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED)(
state
),
email: selectEmailToVerify(state),
user: selectUser(state),
});
const perform = dispatch => () => ({
completeFirstRun: () => {
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
dispatch(doSetClientSetting(SETTINGS.FIRST_RUN_COMPLETED, true));
},
acknowledgeEmail: () => {
dispatch(doSetClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, true));
},
});
export default connect(
select,
perform
)(FirstRunEmailCollection);

View file

@ -0,0 +1,46 @@
// @flow
import React from 'react';
import Button from 'component/button';
import UserEmailNew from 'component/userEmailNew';
import UserEmailVerify from 'component/userEmailVerify';
type Props = {
email: string,
emailCollectionAcknowledged: boolean,
user: ?{ has_verified_email: boolean },
completeFirstRun: () => void,
acknowledgeEmail: () => void,
};
class FirstRunEmailCollection extends React.PureComponent<Props> {
render() {
const {
completeFirstRun,
email,
user,
emailCollectionAcknowledged,
acknowledgeEmail,
} = this.props;
// this shouldn't happen
if (!user) {
return null;
}
const cancelButton = <Button button="link" onClick={completeFirstRun} label={__('Not Now')} />;
if (user && !user.has_verified_email && !email) {
return <UserEmailNew cancelButton={cancelButton} />;
} else if (user && !user.has_verified_email) {
return <UserEmailVerify cancelButton={cancelButton} />;
}
// Try to acknowledge here so users don't see an empty email screen in the first run banner
if (!emailCollectionAcknowledged) {
acknowledgeEmail();
}
return null;
}
}
export default FirstRunEmailCollection;

View file

@ -0,0 +1,29 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectUser } from 'lbryinc';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetClientSetting } from 'redux/actions/settings';
import FirstRun from './view';
const select = state => ({
emailCollectionAcknowledged: makeSelectClientSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED)(
state
),
welcomeAcknowledged: makeSelectClientSetting(SETTINGS.NEW_USER_ACKNOWLEDGED)(state),
firstRunComplete: makeSelectClientSetting(SETTINGS.FIRST_RUN_COMPLETED)(state),
user: selectUser(state),
});
const perform = dispatch => ({
acknowledgeWelcome: () => {
dispatch(doSetClientSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, true));
},
completeFirstRun: () => {
dispatch(doSetClientSetting(SETTINGS.FIRST_RUN_COMPLETED, true));
},
});
export default connect(
select,
perform
)(FirstRun);

View file

@ -0,0 +1,115 @@
// @flow
import React, { PureComponent } from 'react';
import posed from 'react-pose';
import Button from 'component/button';
import EmailCollection from 'component/emailCollection';
import Native from 'native';
//
// Animation for items inside banner
// The height for items must be static (in banner.scss) so that we can reliably animate into the banner and be vertically centered
//
const spring = {
type: 'spring',
stiffness: 100,
damping: 10,
mass: 1,
};
const Welcome = posed.div({
hide: { opacity: 0, y: '310px', ...spring },
show: { opacity: 1, ...spring },
});
const Email = posed.div({
hide: { opacity: 0, y: '0', ...spring },
show: { opacity: 1, y: '-310px', ...spring, delay: 175 },
});
const Help = posed.div({
hide: { opacity: 0, y: '0', ...spring },
show: { opacity: 1, y: '-620px', ...spring, delay: 175 },
});
type Props = {
welcomeAcknowledged: boolean,
emailCollectionAcknowledged: boolean,
firstRunComplete: boolean,
acknowledgeWelcome: () => void,
completeFirstRun: () => void,
};
export default class FirstRun extends PureComponent<Props> {
render() {
const {
welcomeAcknowledged,
emailCollectionAcknowledged,
firstRunComplete,
acknowledgeWelcome,
completeFirstRun,
} = this.props;
if (firstRunComplete) {
return null;
}
const showWelcome = !welcomeAcknowledged;
const showEmail = !emailCollectionAcknowledged && welcomeAcknowledged;
const showHelp = !showWelcome && !showEmail;
return (
<div className="banner banner--first-run">
<img
alt="Friendly gerbil"
className="yrbl--first-run banner__item"
src={Native.imagePath('gerbil-happy.png')}
/>
<div className="banner__item">
<div className="banner__item--static-for-animation">
<Welcome className="banner__content" pose={showWelcome ? 'show' : 'hide'}>
<div>
<header className="card__header">
<h1 className="card__title">{__('Hi There')}</h1>
</header>
<div className="card__content">
<p>
{__('Using LBRY is like dating a centaur. Totally normal up top, and')}{' '}
<em>{__('way different')}</em> {__('underneath.')}
</p>
<p>{__('Up top, LBRY is similar to popular media sites.')}</p>
<p>
{__(
'Below, LBRY is controlled by users -- you -- via blockchain and decentralization.'
)}
</p>
<div className="card__actions">
<Button button="primary" onClick={acknowledgeWelcome} label={__("I'm In")} />
</div>
</div>
</div>
</Welcome>
</div>
<div className="banner__item--static-for-animation">
<Email pose={showEmail ? 'show' : 'hide'}>
<EmailCollection />
</Email>
</div>
<div className="banner__item--static-for-animation">
<Help pose={showHelp ? 'show' : 'hide'}>
<header className="card__header">
<h1 className="card__title">{__('You Are Awesome!')}</h1>
</header>
<div className="card__content">
<p>{__("Check out some of the neat files below me. I'll see you around!")}</p>
<div className="card__actions">
<Button button="primary" onClick={completeFirstRun} label={__('See You Later')} />
</div>
</div>
</Help>
</div>
</div>
</div>
);
}
}

View file

@ -61,7 +61,7 @@ class SocialShare extends React.PureComponent<Props> {
<div className="card__content">
<label className="card__subtitle">{__('Web link')}</label>
<CopyableText copyable={speechURL} />
<div className="card__actions card__actions--center card__actions--top-space">
<div className="card__actions card__actions--center">
<ToolTip onComponent body={__('Facebook')}>
<Button
iconColor="blue"
@ -95,7 +95,7 @@ class SocialShare extends React.PureComponent<Props> {
<div className="card__content">
<label className="card__subtitle">{__('LBRY App link')}</label>
<CopyableText copyable={lbryURL} noSnackbar />
<div className="card__actions card__actions--center card__actions--top-space">
<div className="card__actions card__actions--center">
<ToolTip onComponent body={__('Facebook')}>
<Button
iconColor="blue"

View file

@ -42,16 +42,14 @@ class UserEmailNew extends React.PureComponent<Props, State> {
return (
<React.Fragment>
<p>
{__("We'll let you know about LBRY updates, security issues, and great new content.")}
</p>
<header className="card__header">
<h2 className="card__title">{__("Don't Miss Out")}</h2>
<p className="card__subtitle">
{__("We'll let you know about LBRY updates, security issues, and great new content.")}
</p>
</header>
<p>
{__(
'In addition, your email address will never be sold and you can unsubscribe at any time.'
)}
</p>
<Form onSubmit={this.handleSubmit}>
<Form className="card__content" onSubmit={this.handleSubmit}>
<FormRow>
<FormField
stretch
@ -66,10 +64,13 @@ class UserEmailNew extends React.PureComponent<Props, State> {
</FormRow>
<div className="card__actions">
<Submit label="Submit" disabled={isPending} />
<Submit label="Submit" disabled={isPending || !this.state.email} />
{cancelButton}
</div>
</Form>
<p className="help">
{__('Your email address will never be sold and you can unsubscribe at any time.')}
</p>
</React.Fragment>
);
}

View file

@ -50,27 +50,33 @@ class UserEmailVerify extends React.PureComponent<Props> {
const { cancelButton, email } = this.props;
return (
<div>
<p>
{__('An email was sent to')} {email}.{' '}
{__('Follow the link and you will be good to go. This will update automatically.')}
</p>
<React.Fragment>
<header className="card__header">
<h2 className="card__title">{__('Waiting For Verification')}</h2>
</header>
<div className="card__actions">
<Button
button="primary"
label={__('Resend verification email')}
onClick={this.handleResendVerificationEmail}
/>
{cancelButton}
<div className="card__content">
<p>
{__('An email was sent to')} {email}.{' '}
{__('Follow the link and you will be good to go. This will update automatically.')}
</p>
<div className="card__actions">
<Button
button="primary"
label={__('Resend verification email')}
onClick={this.handleResendVerificationEmail}
/>
{cancelButton}
</div>
<p className="help">
{__('Email')} <Button button="link" href="mailto:help@lbry.io" label="help@lbry.io" />{' '}
or join our <Button button="link" href="https://chat.lbry.io" label="chat" />{' '}
{__('if you encounter any trouble verifying.')}
</p>
</div>
<p className="help">
{__('Email')} <Button button="link" href="mailto:help@lbry.io" label="help@lbry.io" /> or
join our <Button button="link" href="https://chat.lbry.io" label="chat" />{' '}
{__('if you encounter any trouble verifying.')}
</p>
</div>
</React.Fragment>
);
}
}

View file

@ -16,6 +16,7 @@ export const VOLUME_CHANGED = 'VOLUME_CHANGED';
export const ADD_COMMENT = 'ADD_COMMENT';
export const SHOW_MODAL = 'SHOW_MODAL';
export const HIDE_MODAL = 'HIDE_MODAL';
export const ENNNHHHAAANNNCEEE = 'ENNNHHHAAANNNCEEE';
// Navigation
export const CHANGE_AFTER_AUTH_PATH = 'CHANGE_AFTER_AUTH_PATH';

View file

@ -1,9 +1,9 @@
/* hardcoded names still exist for these in reducers/settings.js - only discovered when debugging */
/* Many SETTINGS are stored in the localStorage by their name -
be careful about changing the value of a SETTINGS constant, as doing so can invalidate existing SETTINGS */
export const CREDIT_REQUIRED_ACKNOWLEDGED = 'credit_required_acknowledged';
export const NEW_USER_ACKNOWLEDGED = 'welcome_acknowledged';
export const EMAIL_COLLECTION_ACKNOWLEDGED = 'email_collection_acknowledged';
export const FIRST_RUN_COMPLETED = 'first_run_completed';
export const LANGUAGE = 'language';
export const SHOW_NSFW = 'showNsfw';
export const SHOW_UNAVAILABLE = 'showUnavailable';

View file

@ -1,23 +0,0 @@
import * as settings from 'constants/settings';
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import { doSetClientSetting } from 'redux/actions/settings';
import { selectEmailToVerify, selectUser } from 'lbryinc';
import ModalEmailCollection from './view';
const select = state => ({
email: selectEmailToVerify(state),
user: selectUser(state),
});
const perform = dispatch => () => ({
closeModal: () => {
dispatch(doSetClientSetting(settings.EMAIL_COLLECTION_ACKNOWLEDGED, true));
dispatch(doHideModal());
},
});
export default connect(
select,
perform
)(ModalEmailCollection);

View file

@ -1,54 +0,0 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import Button from 'component/button';
import UserEmailNew from 'component/userEmailNew';
import UserEmailVerify from 'component/userEmailVerify';
type Props = {
closeModal: () => void,
email: string,
user: ?{ has_verified_email: boolean },
};
class ModalEmailCollection extends React.PureComponent<Props> {
getTitle() {
// const { user } = this.props;
//
// if (user && user.email && !user.has_verified_email) {
// return __('Awaiting Confirmation');
// }
return __('Can We Stay In Touch?');
}
renderInner() {
const { closeModal, email, user } = this.props;
const cancelButton = <Button button="link" onClick={closeModal} label={__('Not Now')} />;
if (user && !user.has_verified_email && !email) {
return <UserEmailNew cancelButton={cancelButton} />;
} else if (user && !user.has_verified_email) {
return <UserEmailVerify onModal cancelButton={cancelButton} />;
}
return closeModal();
}
render() {
const { user } = this.props;
// this shouldn't happen
if (!user) {
return null;
}
return (
<Modal type="custom" isOpen contentLabel="Email" title={this.getTitle()}>
<section className="card__content">{this.renderInner()}</section>
</Modal>
);
}
}
export default ModalEmailCollection;

View file

@ -16,7 +16,6 @@ import ModalTransactionFailed from 'modal/modalTransactionFailed';
import ModalFileTimeout from 'modal/modalFileTimeout';
import ModalAffirmPurchase from 'modal/modalAffirmPurchase';
import ModalRevokeClaim from 'modal/modalRevokeClaim';
import ModalEmailCollection from 'modal/modalEmailCollection';
import ModalPhoneCollection from 'modal/modalPhoneCollection';
import ModalFirstSubscription from 'modal/modalFirstSubscription';
import ModalConfirmTransaction from 'modal/modalConfirmTransaction';
@ -78,11 +77,10 @@ class ModalRouter extends React.PureComponent<Props, State> {
return;
}
const transitionModal = [
this.checkShowWelcome,
this.checkShowEmail,
this.checkShowCreditIntro,
].reduce((acc, func) => (!acc ? func.bind(this)(props) : acc), false);
const transitionModal = [this.checkShowCreditIntro].reduce(
(acc, func) => (!acc ? func.bind(this)(props) : acc),
false
);
if (
transitionModal &&
@ -96,30 +94,6 @@ class ModalRouter extends React.PureComponent<Props, State> {
}
}
checkShowWelcome(props: Props) {
const { isWelcomeAcknowledged, user } = props;
if (!isWelcomeAcknowledged && user && !user.is_reward_approved && !user.is_identity_verified) {
return MODALS.WELCOME;
}
return undefined;
}
checkShowEmail(props: Props) {
const { isEmailCollectionAcknowledged, isVerificationCandidate, user } = props;
if (
!isEmailCollectionAcknowledged &&
isVerificationCandidate &&
user &&
!user.has_verified_email
) {
return MODALS.EMAIL_COLLECTION;
}
return undefined;
}
checkShowCreditIntro(props: Props) {
const { balance, page, isCreditIntroAcknowledged } = props;
@ -185,8 +159,6 @@ class ModalRouter extends React.PureComponent<Props, State> {
return <ModalRevokeClaim {...modalProps} />;
case MODALS.PHONE_COLLECTION:
return <ModalPhoneCollection {...modalProps} />;
case MODALS.EMAIL_COLLECTION:
return <ModalEmailCollection {...modalProps} />;
case MODALS.FIRST_SUBSCRIPTION:
return <ModalFirstSubscription {...modalProps} />;
case MODALS.SEND_TIP:

View file

@ -30,19 +30,6 @@ class AuthPage extends React.PureComponent<Props> {
this.navigateIfAuthenticated(nextProps);
}
getTitle() {
const { email, isPending, user } = this.props;
if (isPending || (user && !user.has_verified_email && !email)) {
return __('Human Proofing');
} else if (user && !user.has_verified_email) {
return __('Awaiting Confirmation');
} else if (user && !user.is_identity_verified && !user.is_reward_approved) {
return __('Final Verification');
}
return __('Welcome to LBRY');
}
navigateIfAuthenticated = (props: Props) => {
const { isPending, user, pathAfterAuth, navigate } = props;
if (
@ -78,24 +65,18 @@ class AuthPage extends React.PureComponent<Props> {
<Page>
{useTemplate ? (
<section className="card card--section">
<header className="card__header card__header--flat">
<h2 className="card__title">{this.getTitle()}</h2>
</header>
{innerContent}
<div className="card__content">
{innerContent}
<p className="help">
{`${__(
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.'
)} `}
<Button
button="link"
onClick={() => navigate('/discover')}
label={__('Return home.')}
/>
</p>
</div>
<p className="help">
{`${__(
'This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.'
)} `}
<Button
button="link"
onClick={() => navigate('/discover')}
label={__('Return home.')}
/>
</p>
</section>
) : (
innerContent

View file

@ -2,10 +2,12 @@
import React from 'react';
import Page from 'component/page';
import CategoryList from 'component/categoryList';
import FirstRun from 'component/firstRun';
type Props = {
fetchFeaturedUris: () => void,
fetchRewardedContent: () => void,
fetchRewards: () => void,
fetchingFeaturedUris: boolean,
featuredUris: {},
};
@ -59,6 +61,7 @@ class DiscoverPage extends React.PureComponent<Props> {
const failedToLoad = !fetchingFeaturedUris && !hasContent;
return (
<Page noPadding isLoading={!hasContent && fetchingFeaturedUris}>
<FirstRun />
{hasContent &&
Object.keys(featuredUris).map(
category =>

View file

@ -371,3 +371,9 @@ export function doConditionalAuthNavigate(newSession) {
}
};
}
export function doToggleEnhancedLayout() {
return {
type: ACTIONS.ENNNHHHAAANNNCEEE,
};
}

View file

@ -34,6 +34,7 @@ export type AppState = {
isUpgradeAvailable: ?boolean,
isUpgradeSkipped: ?boolean,
hasClickedComment: boolean,
enhancedLayout: boolean,
};
const defaultState: AppState = {
@ -57,6 +58,7 @@ const defaultState: AppState = {
checkUpgradeTimer: undefined,
isUpgradeAvailable: undefined,
isUpgradeSkipped: undefined,
enhancedLayout: false,
};
reducers[ACTIONS.DAEMON_READY] = state =>
@ -213,6 +215,11 @@ reducers[ACTIONS.AUTHENTICATION_FAILURE] = state =>
modal: MODALS.AUTHENTICATION_FAILURE,
});
reducers[ACTIONS.ENNNHHHAAANNNCEEE] = state =>
Object.assign({}, state, {
enhancedLayout: !state.enhancedLayout,
});
export default function reducer(state: AppState = defaultState, action: any) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -10,24 +10,34 @@ function getLocalStorageSetting(setting, fallback) {
const reducers = {};
const defaultState = {
clientSettings: {
instantPurchaseEnabled: getLocalStorageSetting(SETTINGS.INSTANT_PURCHASE_ENABLED, false),
instantPurchaseMax: getLocalStorageSetting(SETTINGS.INSTANT_PURCHASE_MAX, {
[SETTINGS.INSTANT_PURCHASE_ENABLED]: getLocalStorageSetting(
SETTINGS.INSTANT_PURCHASE_ENABLED,
false
),
[SETTINGS.INSTANT_PURCHASE_MAX]: getLocalStorageSetting(SETTINGS.INSTANT_PURCHASE_MAX, {
currency: 'LBC',
amount: 0.1,
}),
showNsfw: getLocalStorageSetting(SETTINGS.SHOW_NSFW, false),
showUnavailable: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true),
welcome_acknowledged: getLocalStorageSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, false),
email_collection_acknowledged: getLocalStorageSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED),
credit_required_acknowledged: false, // this needs to be re-acknowledged every run
language: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
theme: getLocalStorageSetting(SETTINGS.THEME, 'light'),
themes: getLocalStorageSetting(SETTINGS.THEMES, []),
automaticDarkModeEnabled: getLocalStorageSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false),
autoplay: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
resultCount: Number(getLocalStorageSetting(SETTINGS.RESULT_COUNT, 50)),
autoDownload: getLocalStorageSetting(SETTINGS.AUTO_DOWNLOAD, true),
osNotificationsEnabled: Boolean(
[SETTINGS.SHOW_NSFW]: getLocalStorageSetting(SETTINGS.SHOW_NSFW, false),
[SETTINGS.SHOW_UNAVAILABLE]: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true),
[SETTINGS.NEW_USER_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, false),
[SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED]: getLocalStorageSetting(
SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED,
false
),
[SETTINGS.FIRST_RUN_COMPLETED]: getLocalStorageSetting(SETTINGS.FIRST_RUN_COMPLETED, false),
[SETTINGS.CREDIT_REQUIRED_ACKNOWLEDGED]: false, // this needs to be re-acknowledged every run
[SETTINGS.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
[SETTINGS.THEME]: getLocalStorageSetting(SETTINGS.THEME, 'dark'),
[SETTINGS.THEMES]: getLocalStorageSetting(SETTINGS.THEMES, []),
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: getLocalStorageSetting(
SETTINGS.AUTOMATIC_DARK_MODE_ENABLED,
false
),
[SETTINGS.AUTOPLAY]: getLocalStorageSetting(SETTINGS.AUTOPLAY, false),
[SETTINGS.RESULT_COUNT]: Number(getLocalStorageSetting(SETTINGS.RESULT_COUNT, 50)),
[SETTINGS.AUTO_DOWNLOAD]: getLocalStorageSetting(SETTINGS.AUTO_DOWNLOAD, true),
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: Boolean(
getLocalStorageSetting(SETTINGS.OS_NOTIFICATIONS_ENABLED, true)
),
},

View file

@ -263,3 +263,5 @@ export const selectModal = createSelector(selectState, state => {
modalProps: state.modalProps,
};
});
export const selectEnhancedLayout = createSelector(selectState, state => state.enhancedLayout);

View file

@ -9,6 +9,7 @@
@import 'init/gui';
@import 'component/animation';
@import 'component/badge';
@import 'component/banner';
@import 'component/button';
@import 'component/card';
@import 'component/channel';
@ -45,3 +46,4 @@
@import 'component/toggle';
@import 'component/tooltip';
@import 'component/wunderbar';
@import 'component/yrbl';

View file

@ -21,6 +21,7 @@
.badge--cost:not(.badge--large) {
background-color: $lbry-yellow-2;
color: $lbry-black;
html[data-theme='dark'] & {
background-color: $lbry-yellow-3;

View file

@ -0,0 +1,30 @@
.banner {
display: flex;
overflow: hidden;
background-color: $lbry-black;
color: $lbry-white;
}
.banner--first-run {
height: 310px;
// Adjust this class inside other `.banner--xxx` styles for control over animation
.banner__item--static-for-animation {
height: 310px;
display: flex;
flex-direction: column;
justify-content: center;
}
}
.banner__item {
&:not(:first-child) {
margin-left: var(--spacing-vertical-large);
}
}
.banner__content {
display: flex;
align-items: center;
height: 100%;
}

View file

@ -81,7 +81,7 @@
// C O N T E N T
.card__content {
font-size: 1.15rem;
font-size: 1.25rem;
p:not(:last-child) {
margin-bottom: var(--spacing-vertical-medium);
@ -174,9 +174,13 @@
// S U B T I T L E
.card__subtitle {
font-size: 1.25rem;
font-size: 1.15rem;
margin-bottom: var(--spacing-vertical-small);
p {
margin-bottom: var(--spacing-vertical-small);
}
.badge {
bottom: -0.12rem;
position: relative;

View file

@ -613,10 +613,10 @@
.media-group__header-navigation {
display: flex;
padding-right: var(--spacing-vertical-medium);
padding-right: var(--spacing-vertical-large);
button:not(:last-of-type) {
margin-right: var(--spacing-vertical-tiny);
margin-right: var(--spacing-vertical-medium);
}
svg {

View file

@ -0,0 +1,37 @@
.yrbl {
height: 300px;
}
.yrbl--first-run {
align-self: center;
height: 200px;
width: auto;
margin: 0 var(--spacing-vertical-large);
}
// Get weird here
.yrbl--enhanced {
position: absolute;
z-index: 9999;
height: 95vh;
width: 95vh;
left: 0;
right: 0;
opacity: 0.5;
animation-name: enhancedAnimation;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: ease-out;
}
@-webkit-keyframes enhancedAnimation {
from {
left: 0;
transform: rotate(0deg);
}
to {
left: 50vw;
transform: rotate(360deg);
}
}

View file

@ -150,7 +150,6 @@ input {
.card {
.help {
margin-bottom: 0;
margin-top: var(--spacing-vertical-large);
}
}
@ -160,6 +159,7 @@ input {
color: $lbry-gray-5;
display: block;
padding: 1rem;
margin-top: var(--spacing-vertical-large);
margin-bottom: var(--spacing-vertical-large);
html[data-theme='dark'] & {

View file

@ -0,0 +1,152 @@
/* eslint-disable */
/*
* Konami-JS ~
* :: Now with support for touch events and multiple instances for
* :: those situations that call for multiple easter eggs!
* Code: https://github.com/snaptortoise/konami-js
* Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
* Version: 1.6.2 (7/17/2018)
* Licensed under the MIT License (http://opensource.org/licenses/MIT)
* Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+, Mobile Safari 2.2.1+ and Android
*/
var Konami = function(callback) {
var konami = {
addEvent: function(obj, type, fn, ref_obj) {
if (obj.addEventListener) obj.addEventListener(type, fn, false);
else if (obj.attachEvent) {
// IE
obj['e' + type + fn] = fn;
obj[type + fn] = function() {
obj['e' + type + fn](window.event, ref_obj);
};
obj.attachEvent('on' + type, obj[type + fn]);
}
},
removeEvent: function(obj, eventName, eventCallback) {
if (obj.removeEventListener) {
obj.removeEventListener(eventName, eventCallback);
} else if (obj.attachEvent) {
obj.detachEvent(eventName);
}
},
input: '',
pattern: '38384040373937396665',
keydownHandler: function(e, ref_obj) {
if (ref_obj) {
konami = ref_obj;
} // IE
konami.input += e ? e.keyCode : event.keyCode;
if (konami.input.length > konami.pattern.length) {
konami.input = konami.input.substr(konami.input.length - konami.pattern.length);
}
if (konami.input === konami.pattern) {
konami.code(konami._currentLink);
konami.input = '';
e.preventDefault();
return false;
}
},
load: function(link) {
this._currentLink = link;
this.addEvent(document, 'keydown', this.keydownHandler, this);
this.iphone.load(link);
},
unload: function() {
this.removeEvent(document, 'keydown', this.keydownHandler);
this.iphone.unload();
},
code: function(link) {
window.location = link;
},
iphone: {
start_x: 0,
start_y: 0,
stop_x: 0,
stop_y: 0,
tap: false,
capture: false,
orig_keys: '',
keys: ['UP', 'UP', 'DOWN', 'DOWN', 'LEFT', 'RIGHT', 'LEFT', 'RIGHT', 'TAP', 'TAP'],
input: [],
code: function(link) {
konami.code(link);
},
touchmoveHandler: function(e) {
if (e.touches.length === 1 && konami.iphone.capture === true) {
var touch = e.touches[0];
konami.iphone.stop_x = touch.pageX;
konami.iphone.stop_y = touch.pageY;
konami.iphone.tap = false;
konami.iphone.capture = false;
konami.iphone.check_direction();
}
},
touchendHandler: function() {
konami.iphone.input.push(konami.iphone.check_direction());
if (konami.iphone.input.length > konami.iphone.keys.length) konami.iphone.input.shift();
if (konami.iphone.input.length === konami.iphone.keys.length) {
var match = true;
for (var i = 0; i < konami.iphone.keys.length; i++) {
if (konami.iphone.input[i] !== konami.iphone.keys[i]) {
match = false;
}
}
if (match) {
konami.iphone.code(konami._currentLink);
}
}
},
touchstartHandler: function(e) {
konami.iphone.start_x = e.changedTouches[0].pageX;
konami.iphone.start_y = e.changedTouches[0].pageY;
konami.iphone.tap = true;
konami.iphone.capture = true;
},
load: function(link) {
this.orig_keys = this.keys;
konami.addEvent(document, 'touchmove', this.touchmoveHandler);
konami.addEvent(document, 'touchend', this.touchendHandler, false);
konami.addEvent(document, 'touchstart', this.touchstartHandler);
},
unload: function() {
konami.removeEvent(document, 'touchmove', this.touchmoveHandler);
konami.removeEvent(document, 'touchend', this.touchendHandler);
konami.removeEvent(document, 'touchstart', this.touchstartHandler);
},
check_direction: function() {
x_magnitude = Math.abs(this.start_x - this.stop_x);
y_magnitude = Math.abs(this.start_y - this.stop_y);
x = this.start_x - this.stop_x < 0 ? 'RIGHT' : 'LEFT';
y = this.start_y - this.stop_y < 0 ? 'DOWN' : 'UP';
result = x_magnitude > y_magnitude ? x : y;
result = this.tap === true ? 'TAP' : result;
return result;
},
},
};
typeof callback === 'string' && konami.load(callback);
if (typeof callback === 'function') {
konami.code = callback;
konami.load();
}
return konami;
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = Konami;
} else {
if (typeof define === 'function' && define.amd) {
define([], function() {
return Konami;
});
} else {
window.Konami = Konami;
}
}
/* eslint-enable */

Binary file not shown.

Before

Width:  |  Height:  |  Size: 180 KiB

After

Width:  |  Height:  |  Size: 265 KiB

View file

@ -101,6 +101,16 @@
lodash "^4.2.0"
to-fast-properties "^2.0.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"
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"
"@lbry/color@^1.0.2":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@lbry/color/-/color-1.0.3.tgz#ec22b2c48b0e358759528fb3bbe7ba468d4e41ca"
@ -128,12 +138,33 @@
node-fetch "^2.1.1"
url-template "^2.0.8"
"@popmotion/easing@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@popmotion/easing/-/easing-1.0.1.tgz#48336aea29542113df10aeb81b4fd10f0b95f937"
"@popmotion/popcorn@^0.3.0":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@popmotion/popcorn/-/popcorn-0.3.1.tgz#f0f33fbf7ff66f2cd0bf28be24bee0b1d46b65e8"
dependencies:
"@popmotion/easing" "^1.0.1"
framesync "^4.0.1"
hey-listen "^1.0.5"
style-value-types "^3.0.7"
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz#ecdf48d532c58ea477acfcab80348424f8d0662f"
dependencies:
any-observable "^0.3.0"
"@types/invariant@^2.2.29":
version "2.2.29"
resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.29.tgz#aa845204cd0a289f65d47e0de63a6a815e30cc66"
"@types/node@^10.0.5":
version "10.12.18"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
"@types/node@^8.0.24":
version "8.10.21"
resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.21.tgz#12b3f2359b27aa05a45d886c8ba1eb8d1a77e285"
@ -4222,6 +4253,12 @@ fragment-cache@^0.2.1:
dependencies:
map-cache "^0.2.2"
framesync@^4.0.0, framesync@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/framesync/-/framesync-4.0.1.tgz#ed7791baf0d266f58ab02000456f82cb384815bf"
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"
@ -4667,6 +4704,10 @@ he@1.1.x:
version "1.1.1"
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
hey-listen@^1.0.4, hey-listen@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.5.tgz#6d0a3a2f60177f65bc4404d571a00025bf5dc20e"
highlight.js@^9.3.0:
version "9.12.0"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e"
@ -7322,6 +7363,29 @@ pluralize@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
popmotion-pose@^3.4.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/popmotion-pose/-/popmotion-pose-3.4.1.tgz#e13e52ba9fe02051926ca6313a7615876f459990"
dependencies:
"@popmotion/easing" "^1.0.1"
hey-listen "^1.0.5"
popmotion "^8.5.0"
pose-core "^2.0.0"
style-value-types "^3.0.6"
tslib "^1.9.1"
popmotion@^8.5.0:
version "8.6.0"
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-8.6.0.tgz#14edf50376fc2d656b59e0d46390896577818abb"
dependencies:
"@popmotion/easing" "^1.0.1"
"@popmotion/popcorn" "^0.3.0"
framesync "^4.0.0"
hey-listen "^1.0.5"
style-value-types "^3.0.6"
stylefire "^2.3.4"
tslib "^1.9.1"
portfinder@^1.0.9:
version "1.0.13"
resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.13.tgz#bb32ecd87c27104ae6ee44b5a3ccbf0ebb1aede9"
@ -7330,6 +7394,15 @@ portfinder@^1.0.9:
debug "^2.2.0"
mkdirp "0.5.x"
pose-core@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/pose-core/-/pose-core-2.0.2.tgz#6a0bd1e7218e4bf30be9c26a30be8eeb5b8695df"
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"
@ -7907,6 +7980,15 @@ react-paginate@^5.2.1:
dependencies:
prop-types "^15.6.1"
react-pose@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/react-pose/-/react-pose-4.0.5.tgz#056309b62e32ce0190f90ef35d7ade1b4c463d85"
dependencies:
"@emotion/is-prop-valid" "^0.7.3"
hey-listen "^1.0.5"
popmotion-pose "^3.4.0"
tslib "^1.9.1"
react-redux@^5.0.3:
version "5.0.7"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8"
@ -9260,6 +9342,18 @@ style-loader@^0.20.2:
loader-utils "^1.1.0"
schema-utils "^0.4.5"
style-value-types@^3.0.6, style-value-types@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-3.0.7.tgz#6e7a22cc8b1a4465193268ed66ad5f2a82579054"
stylefire@^2.3.4:
version "2.3.7"
resolved "https://registry.yarnpkg.com/stylefire/-/stylefire-2.3.7.tgz#285b86a48f6c25bbdcccb5dafa26b9f578235bb5"
dependencies:
framesync "^4.0.0"
hey-listen "^1.0.4"
style-value-types "^3.0.6"
sumchecker@^1.2.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/sumchecker/-/sumchecker-1.3.1.tgz#79bb3b4456dd04f18ebdbc0d703a1d1daec5105d"
@ -9592,7 +9686,7 @@ truncate-utf8-bytes@^1.0.0:
dependencies:
utf8-byte-length "^1.0.1"
tslib@^1.9.0:
tslib@^1.9.0, tslib@^1.9.1:
version "1.9.3"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286"