mirror of
https://github.com/LBRYFoundation/lbry-desktop.git
synced 2025-09-11 21:19:43 +00:00
feat: show guided tooltip to invite page on first run
This commit is contained in:
parent
919bd4c934
commit
21661fce85
17 changed files with 346 additions and 250 deletions
|
@ -9,6 +9,7 @@ type Props = {
|
||||||
icon?: boolean,
|
icon?: boolean,
|
||||||
direction: string,
|
direction: string,
|
||||||
onComponent?: boolean, // extra padding to account for button/form field size
|
onComponent?: boolean, // extra padding to account for button/form field size
|
||||||
|
alwaysVisible?: boolean, // should tooltip stay open, guide callbacks will close it manually
|
||||||
};
|
};
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
@ -18,6 +19,7 @@ type State = {
|
||||||
class ToolTip extends React.PureComponent<Props, State> {
|
class ToolTip extends React.PureComponent<Props, State> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
direction: 'bottom',
|
direction: 'bottom',
|
||||||
|
alwaysVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
|
@ -88,7 +90,7 @@ class ToolTip extends React.PureComponent<Props, State> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { direction } = this.state;
|
const { direction } = this.state;
|
||||||
const { children, label, body, icon, onComponent } = this.props;
|
const { children, label, body, icon, onComponent, alwaysVisible } = this.props;
|
||||||
|
|
||||||
const tooltipContent = children || label;
|
const tooltipContent = children || label;
|
||||||
const bodyLength = body.length;
|
const bodyLength = body.length;
|
||||||
|
@ -106,6 +108,7 @@ class ToolTip extends React.PureComponent<Props, State> {
|
||||||
'tooltip--bottom': direction === 'bottom',
|
'tooltip--bottom': direction === 'bottom',
|
||||||
'tooltip--left': direction === 'left',
|
'tooltip--left': direction === 'left',
|
||||||
'tooltip--on-component': onComponent,
|
'tooltip--on-component': onComponent,
|
||||||
|
'tooltip--always-visible': alwaysVisible,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{tooltipContent}
|
{tooltipContent}
|
||||||
|
@ -113,7 +116,7 @@ class ToolTip extends React.PureComponent<Props, State> {
|
||||||
ref={ref => {
|
ref={ref => {
|
||||||
this.tooltip = ref;
|
this.tooltip = ref;
|
||||||
}}
|
}}
|
||||||
className={classnames('tooltip__body', {
|
className={classnames('card tooltip__body', {
|
||||||
'tooltip__body--short': isShortDescription,
|
'tooltip__body--short': isShortDescription,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
|
36
src/renderer/component/common/yrbl.jsx
Normal file
36
src/renderer/component/common/yrbl.jsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import Native from 'native';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string,
|
||||||
|
subtitle: string,
|
||||||
|
type: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const yrblTypes = {
|
||||||
|
happy: 'gerbil-happy.png',
|
||||||
|
sad: 'gerbil-sad.png',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default class extends React.PureComponent<Props> {
|
||||||
|
static defaultProps = {
|
||||||
|
type: 'happy',
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { title, subtitle, type } = this.props;
|
||||||
|
|
||||||
|
const image = yrblTypes[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="yrbl-wrap">
|
||||||
|
<img alt="Friendly gerbil" className="yrbl" src={Native.imagePath(image)} />
|
||||||
|
<div className="card__content">
|
||||||
|
<h2 className="card__title">{title}</h2>
|
||||||
|
<p className="card__subtitle">{subtitle}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import RewardLink from 'component/rewardLink';
|
import RewardLink from 'component/rewardLink';
|
||||||
|
import Yrbl from 'component/common/yrbl';
|
||||||
import { rewards } from 'lbryinc';
|
import { rewards } from 'lbryinc';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -22,6 +23,18 @@ class InviteList extends React.PureComponent<Props> {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!invitees.length) {
|
||||||
|
return (
|
||||||
|
<Yrbl
|
||||||
|
type="happy"
|
||||||
|
title={__('Power To The People')}
|
||||||
|
subtitle={__(
|
||||||
|
'LBRY is powered by the users. More users, more power… and with great power comes great responsibility.'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card card--section">
|
<section className="card card--section">
|
||||||
<header className="card__header">
|
<header className="card__header">
|
||||||
|
@ -29,43 +42,37 @@ class InviteList extends React.PureComponent<Props> {
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="card__content">
|
<div className="card__content">
|
||||||
{invitees.length === 0 && (
|
<table className="table table--stretch">
|
||||||
<span className="empty">{__("You haven't invited anyone.")} </span>
|
<thead>
|
||||||
)}
|
<tr>
|
||||||
{invitees.length > 0 && (
|
<th>{__('Invitee Email')}</th>
|
||||||
<table className="table table--stretch">
|
<th className="text-center">{__('Invite Status')}</th>
|
||||||
<thead>
|
<th className="text-center">{__('Reward')}</th>
|
||||||
<tr>
|
</tr>
|
||||||
<th>{__('Invitee Email')}</th>
|
</thead>
|
||||||
<th className="text-center">{__('Invite Status')}</th>
|
<tbody>
|
||||||
<th className="text-center">{__('Reward')}</th>
|
{invitees.map(invitee => (
|
||||||
|
<tr key={invitee.email}>
|
||||||
|
<td>{invitee.email}</td>
|
||||||
|
<td className="text-center">
|
||||||
|
{invitee.invite_accepted ? (
|
||||||
|
<Icon icon={ICONS.COMPLETED} />
|
||||||
|
) : (
|
||||||
|
<span className="empty">{__('unused')}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
{invitee.invite_reward_claimed && <Icon icon={ICONS.COMPLETED} />}
|
||||||
|
{!invitee.invite_reward_claimed && invitee.invite_reward_claimable ? (
|
||||||
|
<RewardLink label={__('claim')} reward_type={rewards.TYPE_REFERRAL} />
|
||||||
|
) : (
|
||||||
|
<span className="empty">{__('unclaimable')}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
))}
|
||||||
<tbody>
|
</tbody>
|
||||||
{invitees.map(invitee => (
|
</table>
|
||||||
<tr key={invitee.email}>
|
|
||||||
<td>{invitee.email}</td>
|
|
||||||
<td className="text-center">
|
|
||||||
{invitee.invite_accepted ? (
|
|
||||||
<Icon icon={ICONS.COMPLETED} />
|
|
||||||
) : (
|
|
||||||
<span className="empty">{__('unused')}</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="text-center">
|
|
||||||
{invitee.invite_reward_claimed ? (
|
|
||||||
<Icon icon={ICONS.COMPLETED} />
|
|
||||||
) : invitee.invite_reward_claimable ? (
|
|
||||||
<RewardLink label={__('claim')} reward_type={rewards.TYPE_REFERRAL} />
|
|
||||||
) : (
|
|
||||||
<span className="empty">{__('unclaimable')}</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="help">
|
<div className="help">
|
||||||
{__(
|
{__(
|
||||||
|
|
|
@ -1,12 +1,20 @@
|
||||||
// I'll come back to this
|
// @flow
|
||||||
/* eslint-disable */
|
/* eslint-disable react/no-multi-comp */
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BusyIndicator from 'component/common/busy-indicator';
|
|
||||||
import CreditAmount from 'component/common/credit-amount';
|
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import { Form, FormRow, FormField, Submit } from 'component/common/form';
|
import { Form, FormRow, FormField, Submit } from 'component/common/form';
|
||||||
|
|
||||||
class FormInviteNew extends React.PureComponent {
|
type FormProps = {
|
||||||
|
inviteNew: string => void,
|
||||||
|
errorMessage: ?string,
|
||||||
|
isPending: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormState = {
|
||||||
|
email: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
class FormInviteNew extends React.PureComponent<FormProps, FormState> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
@ -14,7 +22,7 @@ class FormInviteNew extends React.PureComponent {
|
||||||
email: '',
|
email: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.handleSubmit = this.handleSubmit.bind(this);
|
(this: any).handleSubmit = this.handleSubmit.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleEmailChanged(event) {
|
handleEmailChanged(event) {
|
||||||
|
@ -56,16 +64,16 @@ class FormInviteNew extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InviteNew extends React.PureComponent {
|
type Props = {
|
||||||
|
errorMessage: ?string,
|
||||||
|
inviteNew: string => void,
|
||||||
|
isPending: boolean,
|
||||||
|
rewardAmount: number,
|
||||||
|
};
|
||||||
|
|
||||||
|
class InviteNew extends React.PureComponent<Props> {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { errorMessage, inviteNew, isPending, rewardAmount } = this.props;
|
||||||
errorMessage,
|
|
||||||
invitesRemaining,
|
|
||||||
inviteNew,
|
|
||||||
inviteStatusIsPending,
|
|
||||||
isPending,
|
|
||||||
rewardAmount,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="card card--section">
|
<section className="card card--section">
|
||||||
|
@ -73,18 +81,10 @@ class InviteNew extends React.PureComponent {
|
||||||
<h2 className="card__title">{__('Invite a Friend')}</h2>
|
<h2 className="card__title">{__('Invite a Friend')}</h2>
|
||||||
|
|
||||||
<p className="card__subtitle">
|
<p className="card__subtitle">
|
||||||
{__("Or an enemy. Or your cousin Jerry, who you're kind of unsure about.")}
|
{__('When your friends start using LBRY, the network gets stronger!')}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/*
|
|
||||||
<div className="card__content">
|
|
||||||
{invitesRemaining > 0 &&
|
|
||||||
<p>{__("You have %s invites remaining.", invitesRemaining)}</p>}
|
|
||||||
{invitesRemaining <= 0 &&
|
|
||||||
<p className="empty">{__("You have no invites.")}</p>}
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
<div className="card__content">
|
<div className="card__content">
|
||||||
<FormInviteNew
|
<FormInviteNew
|
||||||
errorMessage={errorMessage}
|
errorMessage={errorMessage}
|
||||||
|
@ -106,4 +106,4 @@ class InviteNew extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InviteNew;
|
export default InviteNew;
|
||||||
/* eslint-enable */
|
/* eslint-enable react/no-multi-comp */
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectCurrentPage, selectCurrentParams } from 'lbry-redux';
|
import { selectCurrentPage, selectCurrentParams, doToast } from 'lbry-redux';
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
import Router from './view';
|
import Router from './view';
|
||||||
|
|
||||||
|
@ -10,5 +10,5 @@ const select = state => ({
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
select,
|
select,
|
||||||
{ doOpenModal }
|
{ doOpenModal, doToast }
|
||||||
)(Router);
|
)(Router);
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
|
||||||
type SideBarLink = {
|
type SideBarLink = {
|
||||||
label: string,
|
label: string,
|
||||||
|
@ -9,6 +10,7 @@ type SideBarLink = {
|
||||||
active: boolean,
|
active: boolean,
|
||||||
icon: ?string,
|
icon: ?string,
|
||||||
subLinks: Array<SideBarLink>,
|
subLinks: Array<SideBarLink>,
|
||||||
|
guide: ?string,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -19,71 +21,86 @@ type Props = {
|
||||||
unreadSubscriptionTotal: number,
|
unreadSubscriptionTotal: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SideBar = (props: Props) => {
|
class SideBar extends React.PureComponent<Props> {
|
||||||
const { navLinks, unreadSubscriptionTotal } = props;
|
renderNavLink(navLink: SideBarLink) {
|
||||||
|
const { label, path, active, subLinks = [], icon, guide } = navLink;
|
||||||
|
|
||||||
return (
|
const inner = (
|
||||||
<nav className="navigation">
|
<li
|
||||||
<div className="navigation__links">
|
className={classnames('navigation__link', {
|
||||||
{navLinks.primary.map(({ label, path, active, icon }) => (
|
'navigation__link--active': active,
|
||||||
<Button
|
})}
|
||||||
icon={icon}
|
key={label}
|
||||||
className={classnames('navigation__link', {
|
>
|
||||||
'navigation__link--active': active,
|
<Button icon={icon} label={label} navigate={path} />
|
||||||
})}
|
|
||||||
key={path}
|
|
||||||
label={
|
|
||||||
path === '/subscriptions' && unreadSubscriptionTotal
|
|
||||||
? `${label} (${unreadSubscriptionTotal})`
|
|
||||||
: label
|
|
||||||
}
|
|
||||||
navigate={path}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<ul>
|
{
|
||||||
<li className="navigation__link navigation__link--title">Account</li>
|
// The sublinks should be animated on open close
|
||||||
|
// Removing it because the current implementation with CSSTransitionGroup
|
||||||
|
// was really slow and looked pretty bad. Possible fix is upgrading to v2
|
||||||
|
// Not sure if that has better performance
|
||||||
|
}
|
||||||
|
{!!subLinks.length &&
|
||||||
|
active && (
|
||||||
|
<ul key="0" className="navigation__link-items">
|
||||||
|
{subLinks.map(({ active: subLinkActive, label: subLabel, path: subPath }) => (
|
||||||
|
<li
|
||||||
|
className={classnames('navigation__link-item', {
|
||||||
|
'navigation__link-item--active': subLinkActive,
|
||||||
|
})}
|
||||||
|
key={subPath}
|
||||||
|
>
|
||||||
|
{subPath ? (
|
||||||
|
<Button label={subLabel} navigate={subPath} />
|
||||||
|
) : (
|
||||||
|
<span>{subLabel}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
|
||||||
{navLinks.secondary.map(({ label, path, active, subLinks = [], icon }) => (
|
return guide ? (
|
||||||
<li
|
<Tooltip key={guide} alwaysVisible direction="right" body={guide}>
|
||||||
|
{inner}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
inner
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { navLinks, unreadSubscriptionTotal } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="navigation">
|
||||||
|
<div className="navigation__links">
|
||||||
|
{navLinks.primary.map(({ label, path, active, icon }) => (
|
||||||
|
<Button
|
||||||
|
icon={icon}
|
||||||
className={classnames('navigation__link', {
|
className={classnames('navigation__link', {
|
||||||
'navigation__link--active': active,
|
'navigation__link--active': active,
|
||||||
})}
|
})}
|
||||||
key={label}
|
key={path}
|
||||||
>
|
label={
|
||||||
<Button icon={icon} label={label} navigate={path} />
|
path === '/subscriptions' && unreadSubscriptionTotal
|
||||||
|
? `${label} (${unreadSubscriptionTotal})`
|
||||||
{
|
: label
|
||||||
// The sublinks should be animated on open close
|
|
||||||
// Removing it because the current implementation with CSSTransitionGroup
|
|
||||||
// was really slow and looked pretty bad. Possible fix is upgrading to v2
|
|
||||||
// Not sure if that has better performance
|
|
||||||
}
|
}
|
||||||
{!!subLinks.length &&
|
navigate={path}
|
||||||
active && (
|
/>
|
||||||
<ul key="0" className="navigation__link-items">
|
|
||||||
{subLinks.map(({ active: subLinkActive, label: subLabel, path: subPath }) => (
|
|
||||||
<li
|
|
||||||
className={classnames('navigation__link-item', {
|
|
||||||
'navigation__link-item--active': subLinkActive,
|
|
||||||
})}
|
|
||||||
key={subPath}
|
|
||||||
>
|
|
||||||
{subPath ? (
|
|
||||||
<Button label={subLabel} navigate={subPath} />
|
|
||||||
) : (
|
|
||||||
<span>{subLabel}</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
</div>
|
<ul>
|
||||||
</nav>
|
<li className="navigation__link navigation__link--title">Account</li>
|
||||||
);
|
{navLinks.secondary.map(this.renderNavLink)}
|
||||||
};
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default SideBar;
|
export default SideBar;
|
||||||
|
|
20
src/renderer/constants/pages.js
Normal file
20
src/renderer/constants/pages.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
export const AUTH = 'auth';
|
||||||
|
export const BACKUP = 'backup';
|
||||||
|
export const CHANNEL = 'channel';
|
||||||
|
export const DISCOVER = 'discover';
|
||||||
|
export const DOWNLOADED = 'downloaded';
|
||||||
|
export const HELP = 'help';
|
||||||
|
export const HISTORY = 'history';
|
||||||
|
export const INVITE = 'invite';
|
||||||
|
export const PUBLISH = 'publish';
|
||||||
|
export const PUBLISHED = 'published';
|
||||||
|
export const GET_CREDITS = 'getcredits';
|
||||||
|
export const REPORT = 'report';
|
||||||
|
export const REWARDS = 'rewards';
|
||||||
|
export const SEND = 'send';
|
||||||
|
export const SETTINGS = 'settings';
|
||||||
|
export const SHOW = 'show';
|
||||||
|
export const WALLET = 'wallet';
|
||||||
|
export const SUBSCRIPTIONS = 'subscriptions';
|
||||||
|
export const SEARCH = 'search';
|
||||||
|
export const USER_HISTORY = 'user_history';
|
|
@ -4,6 +4,7 @@ export const CREDIT_REQUIRED_ACKNOWLEDGED = 'credit_required_acknowledged';
|
||||||
export const NEW_USER_ACKNOWLEDGED = 'welcome_acknowledged';
|
export const NEW_USER_ACKNOWLEDGED = 'welcome_acknowledged';
|
||||||
export const EMAIL_COLLECTION_ACKNOWLEDGED = 'email_collection_acknowledged';
|
export const EMAIL_COLLECTION_ACKNOWLEDGED = 'email_collection_acknowledged';
|
||||||
export const FIRST_RUN_COMPLETED = 'first_run_completed';
|
export const FIRST_RUN_COMPLETED = 'first_run_completed';
|
||||||
|
export const INVITE_ACKNOWLEDGED = 'invite_acknowledged';
|
||||||
export const LANGUAGE = 'language';
|
export const LANGUAGE = 'language';
|
||||||
export const SHOW_NSFW = 'showNsfw';
|
export const SHOW_NSFW = 'showNsfw';
|
||||||
export const SHOW_UNAVAILABLE = 'showUnavailable';
|
export const SHOW_UNAVAILABLE = 'showUnavailable';
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import {
|
import {
|
||||||
doFetchInviteStatus,
|
doFetchInviteStatus,
|
||||||
selectUserInviteStatusFailed,
|
selectUserInviteStatusFailed,
|
||||||
selectUserInviteStatusIsPending,
|
selectUserInviteStatusIsPending,
|
||||||
} from 'lbryinc';
|
} from 'lbryinc';
|
||||||
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { doSetClientSetting } from 'redux/actions/settings';
|
||||||
import InvitePage from './view';
|
import InvitePage from './view';
|
||||||
|
|
||||||
const select = state => ({
|
const select = state => ({
|
||||||
isFailed: selectUserInviteStatusFailed(state),
|
isFailed: selectUserInviteStatusFailed(state),
|
||||||
isPending: selectUserInviteStatusIsPending(state),
|
isPending: selectUserInviteStatusIsPending(state),
|
||||||
|
inviteAcknowledged: makeSelectClientSetting(state)(SETTINGS.INVITE_ACKNOWLEDGED),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
fetchInviteStatus: () => dispatch(doFetchInviteStatus()),
|
fetchInviteStatus: () => dispatch(doFetchInviteStatus()),
|
||||||
|
acknowledgeInivte: () => dispatch(doSetClientSetting(SETTINGS.INVITE_ACKNOWLEDGED, true)),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(
|
export default connect(
|
||||||
|
|
|
@ -1,12 +1,26 @@
|
||||||
|
// @flow
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BusyIndicator from 'component/common/busy-indicator';
|
import BusyIndicator from 'component/common/busy-indicator';
|
||||||
import InviteNew from 'component/inviteNew';
|
import InviteNew from 'component/inviteNew';
|
||||||
import InviteList from 'component/inviteList';
|
import InviteList from 'component/inviteList';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
|
|
||||||
class InvitePage extends React.PureComponent {
|
type Props = {
|
||||||
componentWillMount() {
|
isPending: boolean,
|
||||||
this.props.fetchInviteStatus();
|
isFailed: boolean,
|
||||||
|
inviteAcknowledged: boolean,
|
||||||
|
acknowledgeInivte: () => void,
|
||||||
|
fetchInviteStatus: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
class InvitePage extends React.PureComponent<Props> {
|
||||||
|
componentDidMount() {
|
||||||
|
const { fetchInviteStatus, inviteAcknowledged, acknowledgeInivte } = this.props;
|
||||||
|
fetchInviteStatus();
|
||||||
|
|
||||||
|
if (!inviteAcknowledged) {
|
||||||
|
acknowledgeInivte();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -43,17 +43,18 @@ export default (props: Props) => {
|
||||||
<Button button="primary" label={__('Explore')} onClick={doShowSuggestedSubs} />
|
<Button button="primary" label={__('Explore')} onClick={doShowSuggestedSubs} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showSuggested && numberOfSubscriptions > 0 && (
|
{showSuggested &&
|
||||||
<div className="card__actions">
|
numberOfSubscriptions > 0 && (
|
||||||
<Button
|
<div className="card__actions">
|
||||||
button="primary"
|
<Button
|
||||||
onClick={onFinish}
|
button="primary"
|
||||||
label={`${__('View your')} ${numberOfSubscriptions} ${
|
onClick={onFinish}
|
||||||
numberOfSubscriptions > 1 ? __('subscribed channels') : __('subscribed channel')
|
label={`${__('View your')} ${numberOfSubscriptions} ${
|
||||||
}`}
|
numberOfSubscriptions > 1 ? __('subscribed channels') : __('subscribed channel')
|
||||||
/>
|
}`}
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showSuggested && !loadingSuggested && <SuggestedSubscriptions />}
|
{showSuggested && !loadingSuggested && <SuggestedSubscriptions />}
|
||||||
|
|
|
@ -9,10 +9,10 @@ import FileList from 'component/fileList';
|
||||||
import { FormField } from 'component/common/form';
|
import { FormField } from 'component/common/form';
|
||||||
import FileCard from 'component/fileCard';
|
import FileCard from 'component/fileCard';
|
||||||
import { parseURI } from 'lbry-redux';
|
import { parseURI } from 'lbry-redux';
|
||||||
import Native from 'native';
|
|
||||||
import SuggestedSubscriptions from 'component/subscribeSuggested';
|
import SuggestedSubscriptions from 'component/subscribeSuggested';
|
||||||
import MarkAsRead from 'component/subscribeMarkAsRead';
|
import MarkAsRead from 'component/subscribeMarkAsRead';
|
||||||
import Tooltip from 'component/common/tooltip';
|
import Tooltip from 'component/common/tooltip';
|
||||||
|
import Yrbl from 'component/common/yrbl';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
viewMode: ViewMode,
|
viewMode: ViewMode,
|
||||||
|
@ -79,17 +79,11 @@ export default (props: Props) => {
|
||||||
|
|
||||||
{!hasSubscriptions && (
|
{!hasSubscriptions && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="yrbl-wrap">
|
<Yrbl
|
||||||
<img
|
type="sad"
|
||||||
alt="Sad gerbil"
|
title={__('Oh no! What happened to your subscriptions?')}
|
||||||
className="subscriptions__gerbil"
|
subtitle={__('These channels look pretty cool.')}
|
||||||
src={Native.imagePath('gerbil-sad.png')}
|
/>
|
||||||
/>
|
|
||||||
<div className="card__content">
|
|
||||||
<h2 className="card__title">{__('Oh no! What happened to your subscriptions?')}</h2>
|
|
||||||
<p className="card__subtitle">{__('These channels look pretty cool.')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SuggestedSubscriptions />
|
<SuggestedSubscriptions />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
|
@ -133,17 +127,10 @@ export default (props: Props) => {
|
||||||
})
|
})
|
||||||
) : (
|
) : (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<div className="yrbl-wrap">
|
<Yrbl
|
||||||
<img
|
title={__('All caught up!')}
|
||||||
alt="Friendly gerbil"
|
subtitle={__('You might like the channels below.')}
|
||||||
className="subscriptions__gerbil"
|
/>
|
||||||
src={Native.imagePath('gerbil-happy.png')}
|
|
||||||
/>
|
|
||||||
<div className="card__content">
|
|
||||||
<h2 className="card__title">{__('All caught up!')}</h2>
|
|
||||||
<p className="card__subtitle">{__('You might like the channels below.')}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<SuggestedSubscriptions />
|
<SuggestedSubscriptions />
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -25,6 +25,7 @@ const defaultState = {
|
||||||
SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED,
|
SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
[SETTINGS.INVITE_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.INVITE_ACKNOWLEDGED, false),
|
||||||
[SETTINGS.FIRST_RUN_COMPLETED]: getLocalStorageSetting(SETTINGS.FIRST_RUN_COMPLETED, 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.CREDIT_REQUIRED_ACKNOWLEDGED]: false, // this needs to be re-acknowledged every run
|
||||||
[SETTINGS.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
|
[SETTINGS.LANGUAGE]: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'),
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { selectCurrentPage, selectHistoryStack } from 'lbry-redux';
|
import { selectCurrentPage, selectHistoryStack } from 'lbry-redux';
|
||||||
import * as icons from 'constants/icons';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
|
||||||
export const selectState = state => state.app || {};
|
export const selectState = state => state.app || {};
|
||||||
|
|
||||||
|
@ -97,15 +100,29 @@ export const selectUpgradeTimer = createSelector(selectState, state => state.che
|
||||||
export const selectNavLinks = createSelector(
|
export const selectNavLinks = createSelector(
|
||||||
selectCurrentPage,
|
selectCurrentPage,
|
||||||
selectHistoryStack,
|
selectHistoryStack,
|
||||||
(currentPage, historyStack) => {
|
makeSelectClientSetting(SETTINGS.FIRST_RUN_COMPLETED),
|
||||||
const isWalletPage = page =>
|
makeSelectClientSetting(SETTINGS.INVITE_ACKNOWLEDGED),
|
||||||
page === 'wallet' ||
|
(currentPage, historyStack, firstRunCompleted, inviteAcknowledged) => {
|
||||||
page === 'send' ||
|
// Determine if any links should show a tooltip for a guided tour
|
||||||
page === 'getcredits' ||
|
// It will only show one at a time, in the order they are set.
|
||||||
page === 'rewards' ||
|
const guidedTourItem = [
|
||||||
page === 'history' ||
|
{
|
||||||
page === 'backup';
|
page: PAGES.INVITE,
|
||||||
|
hasBeenCompleted: inviteAcknowledged,
|
||||||
|
guide: 'Check this out!',
|
||||||
|
},
|
||||||
|
// Add more items below for tooltip guides that will happen after a user has completed the invite guide
|
||||||
|
].filter(({ hasBeenCompleted }) => !hasBeenCompleted)[0];
|
||||||
|
|
||||||
|
const isWalletPage = page =>
|
||||||
|
page === PAGES.WALLET ||
|
||||||
|
page === PAGES.SEND ||
|
||||||
|
page === PAGES.GET_CREDITS ||
|
||||||
|
page === PAGES.REWARDS ||
|
||||||
|
page === PAGES.HISTORY ||
|
||||||
|
page === PAGES.BACKUP;
|
||||||
|
|
||||||
|
const isCurrentlyWalletPage = isWalletPage(currentPage);
|
||||||
const previousStack = historyStack.slice().reverse();
|
const previousStack = historyStack.slice().reverse();
|
||||||
|
|
||||||
const getPreviousSubLinkPath = checkIfValidPage => {
|
const getPreviousSubLinkPath = checkIfValidPage => {
|
||||||
|
@ -124,107 +141,92 @@ export const selectNavLinks = createSelector(
|
||||||
|
|
||||||
// Gets the last active sublink in a section
|
// Gets the last active sublink in a section
|
||||||
const getActiveSublink = category => {
|
const getActiveSublink = category => {
|
||||||
if (category === 'wallet') {
|
if (category === PAGES.WALLET) {
|
||||||
const previousPath = getPreviousSubLinkPath(isWalletPage);
|
const previousPath = getPreviousSubLinkPath(isWalletPage);
|
||||||
return previousPath || '/wallet';
|
return previousPath || `/${PAGES.WALLET}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isCurrentlyWalletPage = isWalletPage(currentPage);
|
// Is this path the first unacknowledged item in the guided tour list
|
||||||
|
const getGuideIfNecessary = page => {
|
||||||
|
if (!firstRunCompleted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return guidedTourItem && guidedTourItem.page === page ? guidedTourItem.guide : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildLink = (label, page) => ({
|
||||||
|
label,
|
||||||
|
path: `/${page}`,
|
||||||
|
active: currentPage === page,
|
||||||
|
guide: getGuideIfNecessary(page),
|
||||||
|
});
|
||||||
|
|
||||||
const walletSubLinks = [
|
const walletSubLinks = [
|
||||||
{
|
{
|
||||||
label: 'Overview',
|
...buildLink('Overview', PAGES.WALLET),
|
||||||
path: '/wallet',
|
|
||||||
active: currentPage === 'wallet',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Send & Receive',
|
...buildLink('Send & Receive', PAGES.SEND),
|
||||||
path: '/send',
|
|
||||||
active: currentPage === 'send',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Transactions',
|
...buildLink('Transactions', PAGES.HISTORY),
|
||||||
path: '/history',
|
|
||||||
active: currentPage === 'history',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Get Credits',
|
...buildLink('Get Credits', PAGES.GET_CREDITS),
|
||||||
path: '/getcredits',
|
|
||||||
active: currentPage === 'getcredits',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Rewards',
|
...buildLink('Rewards', PAGES.REWARDS),
|
||||||
path: '/rewards',
|
|
||||||
active: currentPage === 'rewards',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Backup',
|
...buildLink('Backup', PAGES.BACKUP),
|
||||||
path: '/backup',
|
|
||||||
active: currentPage === 'backup',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const navLinks = {
|
const navLinks = {
|
||||||
primary: [
|
primary: [
|
||||||
{
|
{
|
||||||
label: 'Explore',
|
...buildLink('Explore', PAGES.DISCOVER),
|
||||||
path: '/discover',
|
icon: ICONS.HOME,
|
||||||
active: currentPage === 'discover',
|
|
||||||
icon: icons.HOME,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Subscriptions',
|
...buildLink('Subscriptions', PAGES.SUBSCRIPTIONS),
|
||||||
path: '/subscriptions',
|
icon: ICONS.SUBSCRIPTION,
|
||||||
active: currentPage === 'subscriptions',
|
|
||||||
icon: icons.SUBSCRIPTION,
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
secondary: [
|
secondary: [
|
||||||
{
|
{
|
||||||
label: 'Wallet',
|
label: 'Wallet',
|
||||||
icon: icons.WALLET,
|
icon: ICONS.WALLET,
|
||||||
subLinks: walletSubLinks,
|
subLinks: walletSubLinks,
|
||||||
path: isCurrentlyWalletPage ? '/wallet' : getActiveSublink('wallet'),
|
path: isCurrentlyWalletPage ? `/${PAGES.WALLET}` : getActiveSublink(PAGES.WALLET),
|
||||||
active: isWalletPage(currentPage),
|
active: isWalletPage(currentPage),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Invite',
|
...buildLink('Invite', PAGES.INVITE),
|
||||||
icon: icons.INVITE,
|
icon: ICONS.INVITE,
|
||||||
path: '/invite',
|
|
||||||
active: currentPage === 'invite',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Downloads',
|
...buildLink('Downloads', PAGES.DOWNLOADED),
|
||||||
icon: icons.LOCAL,
|
icon: ICONS.LOCAL,
|
||||||
path: '/downloaded',
|
|
||||||
active: currentPage === 'downloaded',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Publishes',
|
...buildLink('Publishes', PAGES.PUBLISHED),
|
||||||
icon: icons.PUBLISHED,
|
icon: ICONS.PUBLISHED,
|
||||||
path: '/published',
|
|
||||||
active: currentPage === 'published',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'History',
|
...buildLink('History', PAGES.USER_HISTORY),
|
||||||
icon: icons.HISTORY,
|
icon: ICONS.HISTORY,
|
||||||
path: '/user_history',
|
|
||||||
active: currentPage === 'user_history',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
...buildLink('Settings', PAGES.SETTINGS),
|
||||||
icon: icons.SETTINGS,
|
icon: ICONS.SETTINGS,
|
||||||
path: '/settings',
|
|
||||||
active: currentPage === 'settings',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Help',
|
...buildLink('Help', PAGES.HELP),
|
||||||
path: '/help',
|
icon: ICONS.HELP,
|
||||||
icon: icons.HELP,
|
|
||||||
active: currentPage === 'help',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,23 +1,8 @@
|
||||||
// The gerbil is tied to subscriptions currently, but this style should move to it's own file once
|
// The gerbil is tied to subscriptions currently, but this style should move to it's own file once
|
||||||
// the gerbil is added in more places with different layouts
|
// the gerbil is added in more places with different layouts
|
||||||
.subscriptions__gerbil {
|
|
||||||
}
|
|
||||||
|
|
||||||
.subscriptions__suggested {
|
.subscriptions__suggested {
|
||||||
animation: expand 0.2s;
|
animation: expand 0.2s;
|
||||||
left: -2rem;
|
left: -2rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: calc(100% + 4rem);
|
width: calc(100% + 4rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
.yrbl-wrap {
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-bottom: var(--spacing-vertical-large);
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 300px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
.tooltip {
|
.tooltip {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
&:not(:hover) {
|
.tooltip__body {
|
||||||
.tooltip__body {
|
visibility: hidden;
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -15,17 +14,20 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip__body {
|
.tooltip__body {
|
||||||
background-color: $lbry-gray-5;
|
|
||||||
border-radius: 8px;
|
|
||||||
color: $lbry-white;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
color: $lbry-black;
|
||||||
|
font-weight: 400;
|
||||||
padding: var(--spacing-vertical-small);
|
padding: var(--spacing-vertical-small);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
width: 200px;
|
width: 200px;
|
||||||
z-index: 1;
|
box-shadow: 5px 5px 5px rgba($lbry-black, 0.15);
|
||||||
|
|
||||||
|
html[data-theme='dark'] & {
|
||||||
|
border: 1px solid #2f2f2f;
|
||||||
|
background-color: $lbry-gray-1;
|
||||||
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
width: 0;
|
width: 0;
|
||||||
|
@ -37,12 +39,18 @@
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--short {
|
&.tooltip__body--short {
|
||||||
width: 130px;
|
width: 130px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tooltip--always-visible {
|
||||||
|
.tooltip__body {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tooltip--bottom .tooltip__body {
|
.tooltip--bottom .tooltip__body {
|
||||||
top: 90%;
|
top: 90%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
|
@ -1,10 +1,19 @@
|
||||||
|
.yrbl-wrap {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-bottom: var(--spacing-vertical-large);
|
||||||
|
}
|
||||||
|
|
||||||
.yrbl {
|
.yrbl {
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
margin-right: var(--spacing-vertical-large);
|
||||||
}
|
}
|
||||||
|
|
||||||
.yrbl--first-run {
|
.yrbl--first-run {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
height: 200px;
|
height: 250px;
|
||||||
width: auto;
|
width: auto;
|
||||||
margin: 0 var(--spacing-vertical-large);
|
margin: 0 var(--spacing-vertical-large);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue