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>
<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,7 +50,12 @@ class UserEmailVerify extends React.PureComponent<Props> {
const { cancelButton, email } = this.props;
return (
<div>
<React.Fragment>
<header className="card__header">
<h2 className="card__title">{__('Waiting For Verification')}</h2>
</header>
<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.')}
@ -66,11 +71,12 @@ class UserEmailVerify extends React.PureComponent<Props> {
</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" />{' '}
{__('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,11 +65,6 @@ 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>
<div className="card__content">
{innerContent}
<p className="help">
@ -95,7 +77,6 @@ class AuthPage extends React.PureComponent<Props> {
label={__('Return home.')}
/>
</p>
</div>
</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"