From 21661fce856928d23546c93411bb6e56465d0efd Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Sat, 26 Jan 2019 19:23:47 -0500 Subject: [PATCH] feat: show guided tooltip to invite page on first run --- src/renderer/component/common/tooltip.jsx | 7 +- src/renderer/component/common/yrbl.jsx | 36 +++++ src/renderer/component/inviteList/view.jsx | 79 ++++++----- src/renderer/component/inviteNew/view.jsx | 50 +++---- src/renderer/component/router/index.js | 4 +- src/renderer/component/sideBar/view.jsx | 133 ++++++++++-------- src/renderer/constants/pages.js | 20 +++ src/renderer/constants/settings.js | 1 + src/renderer/page/invite/index.js | 5 + src/renderer/page/invite/view.jsx | 20 ++- .../page/subscriptions/internal/first-run.jsx | 23 +-- .../internal/user-subscriptions.jsx | 33 ++--- src/renderer/redux/reducers/settings.js | 1 + src/renderer/redux/selectors/app.js | 130 ++++++++--------- .../scss/component/_subscriptions.scss | 15 -- src/renderer/scss/component/_tooltip.scss | 28 ++-- src/renderer/scss/component/_yrbl.scss | 11 +- 17 files changed, 346 insertions(+), 250 deletions(-) create mode 100644 src/renderer/component/common/yrbl.jsx create mode 100644 src/renderer/constants/pages.js diff --git a/src/renderer/component/common/tooltip.jsx b/src/renderer/component/common/tooltip.jsx index 5d5aacadc..eec57fc99 100644 --- a/src/renderer/component/common/tooltip.jsx +++ b/src/renderer/component/common/tooltip.jsx @@ -9,6 +9,7 @@ type Props = { icon?: boolean, direction: string, 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 = { @@ -18,6 +19,7 @@ type State = { class ToolTip extends React.PureComponent { static defaultProps = { direction: 'bottom', + alwaysVisible: false, }; constructor(props: Props) { @@ -88,7 +90,7 @@ class ToolTip extends React.PureComponent { render() { 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 bodyLength = body.length; @@ -106,6 +108,7 @@ class ToolTip extends React.PureComponent { 'tooltip--bottom': direction === 'bottom', 'tooltip--left': direction === 'left', 'tooltip--on-component': onComponent, + 'tooltip--always-visible': alwaysVisible, })} > {tooltipContent} @@ -113,7 +116,7 @@ class ToolTip extends React.PureComponent { ref={ref => { this.tooltip = ref; }} - className={classnames('tooltip__body', { + className={classnames('card tooltip__body', { 'tooltip__body--short': isShortDescription, })} > diff --git a/src/renderer/component/common/yrbl.jsx b/src/renderer/component/common/yrbl.jsx new file mode 100644 index 000000000..e982d2b69 --- /dev/null +++ b/src/renderer/component/common/yrbl.jsx @@ -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 { + static defaultProps = { + type: 'happy', + }; + + render() { + const { title, subtitle, type } = this.props; + + const image = yrblTypes[type]; + + return ( +
+ Friendly gerbil +
+

{title}

+

{subtitle}

+
+
+ ); + } +} diff --git a/src/renderer/component/inviteList/view.jsx b/src/renderer/component/inviteList/view.jsx index 5dc120892..5872b4f44 100644 --- a/src/renderer/component/inviteList/view.jsx +++ b/src/renderer/component/inviteList/view.jsx @@ -3,6 +3,7 @@ import * as ICONS from 'constants/icons'; import React from 'react'; import Icon from 'component/common/icon'; import RewardLink from 'component/rewardLink'; +import Yrbl from 'component/common/yrbl'; import { rewards } from 'lbryinc'; type Props = { @@ -22,6 +23,18 @@ class InviteList extends React.PureComponent { return null; } + if (!invitees.length) { + return ( + + ); + } + return (
@@ -29,43 +42,37 @@ class InviteList extends React.PureComponent {
- {invitees.length === 0 && ( - {__("You haven't invited anyone.")} - )} - {invitees.length > 0 && ( - - - - - - +
{__('Invitee Email')}{__('Invite Status')}{__('Reward')}
+ + + + + + + + + {invitees.map(invitee => ( + + + + - - - {invitees.map(invitee => ( - - - - - - ))} - -
{__('Invitee Email')}{__('Invite Status')}{__('Reward')}
{invitee.email} + {invitee.invite_accepted ? ( + + ) : ( + {__('unused')} + )} + + {invitee.invite_reward_claimed && } + {!invitee.invite_reward_claimed && invitee.invite_reward_claimable ? ( + + ) : ( + {__('unclaimable')} + )} +
{invitee.email} - {invitee.invite_accepted ? ( - - ) : ( - {__('unused')} - )} - - {invitee.invite_reward_claimed ? ( - - ) : invitee.invite_reward_claimable ? ( - - ) : ( - {__('unclaimable')} - )} -
- )} + ))} + +
{__( diff --git a/src/renderer/component/inviteNew/view.jsx b/src/renderer/component/inviteNew/view.jsx index 65ddade31..c0381723e 100644 --- a/src/renderer/component/inviteNew/view.jsx +++ b/src/renderer/component/inviteNew/view.jsx @@ -1,12 +1,20 @@ -// I'll come back to this -/* eslint-disable */ +// @flow +/* eslint-disable react/no-multi-comp */ import React from 'react'; -import BusyIndicator from 'component/common/busy-indicator'; -import CreditAmount from 'component/common/credit-amount'; import Button from 'component/button'; 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 { constructor() { super(); @@ -14,7 +22,7 @@ class FormInviteNew extends React.PureComponent { email: '', }; - this.handleSubmit = this.handleSubmit.bind(this); + (this: any).handleSubmit = this.handleSubmit.bind(this); } 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 { render() { - const { - errorMessage, - invitesRemaining, - inviteNew, - inviteStatusIsPending, - isPending, - rewardAmount, - } = this.props; + const { errorMessage, inviteNew, isPending, rewardAmount } = this.props; return (
@@ -73,18 +81,10 @@ class InviteNew extends React.PureComponent {

{__('Invite a Friend')}

- {__("Or an enemy. Or your cousin Jerry, who you're kind of unsure about.")} + {__('When your friends start using LBRY, the network gets stronger!')}

- {/* -
- {invitesRemaining > 0 && -

{__("You have %s invites remaining.", invitesRemaining)}

} - {invitesRemaining <= 0 && -

{__("You have no invites.")}

} -
*/} -
({ export default connect( select, - { doOpenModal } + { doOpenModal, doToast } )(Router); diff --git a/src/renderer/component/sideBar/view.jsx b/src/renderer/component/sideBar/view.jsx index 879bb082d..94394bf17 100644 --- a/src/renderer/component/sideBar/view.jsx +++ b/src/renderer/component/sideBar/view.jsx @@ -2,6 +2,7 @@ import * as React from 'react'; import Button from 'component/button'; import classnames from 'classnames'; +import Tooltip from 'component/common/tooltip'; type SideBarLink = { label: string, @@ -9,6 +10,7 @@ type SideBarLink = { active: boolean, icon: ?string, subLinks: Array, + guide: ?string, }; type Props = { @@ -19,71 +21,86 @@ type Props = { unreadSubscriptionTotal: number, }; -const SideBar = (props: Props) => { - const { navLinks, unreadSubscriptionTotal } = props; +class SideBar extends React.PureComponent { + renderNavLink(navLink: SideBarLink) { + const { label, path, active, subLinks = [], icon, guide } = navLink; - return ( - - ); -}; + +
    +
  • Account
  • + {navLinks.secondary.map(this.renderNavLink)} +
+
+ + ); + } +} export default SideBar; diff --git a/src/renderer/constants/pages.js b/src/renderer/constants/pages.js new file mode 100644 index 000000000..2ef273a29 --- /dev/null +++ b/src/renderer/constants/pages.js @@ -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'; diff --git a/src/renderer/constants/settings.js b/src/renderer/constants/settings.js index 3f039796b..a00c97a9b 100644 --- a/src/renderer/constants/settings.js +++ b/src/renderer/constants/settings.js @@ -4,6 +4,7 @@ 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 INVITE_ACKNOWLEDGED = 'invite_acknowledged'; export const LANGUAGE = 'language'; export const SHOW_NSFW = 'showNsfw'; export const SHOW_UNAVAILABLE = 'showUnavailable'; diff --git a/src/renderer/page/invite/index.js b/src/renderer/page/invite/index.js index c69a52697..061bb67c1 100644 --- a/src/renderer/page/invite/index.js +++ b/src/renderer/page/invite/index.js @@ -1,18 +1,23 @@ +import * as SETTINGS from 'constants/settings'; import { connect } from 'react-redux'; import { doFetchInviteStatus, selectUserInviteStatusFailed, selectUserInviteStatusIsPending, } from 'lbryinc'; +import { makeSelectClientSetting } from 'redux/selectors/settings'; +import { doSetClientSetting } from 'redux/actions/settings'; import InvitePage from './view'; const select = state => ({ isFailed: selectUserInviteStatusFailed(state), isPending: selectUserInviteStatusIsPending(state), + inviteAcknowledged: makeSelectClientSetting(state)(SETTINGS.INVITE_ACKNOWLEDGED), }); const perform = dispatch => ({ fetchInviteStatus: () => dispatch(doFetchInviteStatus()), + acknowledgeInivte: () => dispatch(doSetClientSetting(SETTINGS.INVITE_ACKNOWLEDGED, true)), }); export default connect( diff --git a/src/renderer/page/invite/view.jsx b/src/renderer/page/invite/view.jsx index 04aadbe46..550569eb5 100644 --- a/src/renderer/page/invite/view.jsx +++ b/src/renderer/page/invite/view.jsx @@ -1,12 +1,26 @@ +// @flow import React from 'react'; import BusyIndicator from 'component/common/busy-indicator'; import InviteNew from 'component/inviteNew'; import InviteList from 'component/inviteList'; import Page from 'component/page'; -class InvitePage extends React.PureComponent { - componentWillMount() { - this.props.fetchInviteStatus(); +type Props = { + isPending: boolean, + isFailed: boolean, + inviteAcknowledged: boolean, + acknowledgeInivte: () => void, + fetchInviteStatus: () => void, +}; + +class InvitePage extends React.PureComponent { + componentDidMount() { + const { fetchInviteStatus, inviteAcknowledged, acknowledgeInivte } = this.props; + fetchInviteStatus(); + + if (!inviteAcknowledged) { + acknowledgeInivte(); + } } render() { diff --git a/src/renderer/page/subscriptions/internal/first-run.jsx b/src/renderer/page/subscriptions/internal/first-run.jsx index a5999f42d..12b2f330f 100644 --- a/src/renderer/page/subscriptions/internal/first-run.jsx +++ b/src/renderer/page/subscriptions/internal/first-run.jsx @@ -43,17 +43,18 @@ export default (props: Props) => {
)} - {showSuggested && numberOfSubscriptions > 0 && ( -
-
- )} + {showSuggested && + numberOfSubscriptions > 0 && ( +
+
+ )}
{showSuggested && !loadingSuggested && } diff --git a/src/renderer/page/subscriptions/internal/user-subscriptions.jsx b/src/renderer/page/subscriptions/internal/user-subscriptions.jsx index ad58fa079..b3373f044 100644 --- a/src/renderer/page/subscriptions/internal/user-subscriptions.jsx +++ b/src/renderer/page/subscriptions/internal/user-subscriptions.jsx @@ -9,10 +9,10 @@ import FileList from 'component/fileList'; import { FormField } from 'component/common/form'; import FileCard from 'component/fileCard'; import { parseURI } from 'lbry-redux'; -import Native from 'native'; import SuggestedSubscriptions from 'component/subscribeSuggested'; import MarkAsRead from 'component/subscribeMarkAsRead'; import Tooltip from 'component/common/tooltip'; +import Yrbl from 'component/common/yrbl'; type Props = { viewMode: ViewMode, @@ -79,17 +79,11 @@ export default (props: Props) => { {!hasSubscriptions && ( -
- Sad gerbil -
-

{__('Oh no! What happened to your subscriptions?')}

-

{__('These channels look pretty cool.')}

-
-
+
)} @@ -133,17 +127,10 @@ export default (props: Props) => { }) ) : ( -
- Friendly gerbil -
-

{__('All caught up!')}

-

{__('You might like the channels below.')}

-
-
+
)} diff --git a/src/renderer/redux/reducers/settings.js b/src/renderer/redux/reducers/settings.js index 03649a882..e61542429 100644 --- a/src/renderer/redux/reducers/settings.js +++ b/src/renderer/redux/reducers/settings.js @@ -25,6 +25,7 @@ const defaultState = { SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED, false ), + [SETTINGS.INVITE_ACKNOWLEDGED]: getLocalStorageSetting(SETTINGS.INVITE_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'), diff --git a/src/renderer/redux/selectors/app.js b/src/renderer/redux/selectors/app.js index f87b86732..7ba299e86 100644 --- a/src/renderer/redux/selectors/app.js +++ b/src/renderer/redux/selectors/app.js @@ -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 { selectCurrentPage, selectHistoryStack } from 'lbry-redux'; -import * as icons from 'constants/icons'; +import { makeSelectClientSetting } from 'redux/selectors/settings'; export const selectState = state => state.app || {}; @@ -97,15 +100,29 @@ export const selectUpgradeTimer = createSelector(selectState, state => state.che export const selectNavLinks = createSelector( selectCurrentPage, selectHistoryStack, - (currentPage, historyStack) => { - const isWalletPage = page => - page === 'wallet' || - page === 'send' || - page === 'getcredits' || - page === 'rewards' || - page === 'history' || - page === 'backup'; + makeSelectClientSetting(SETTINGS.FIRST_RUN_COMPLETED), + makeSelectClientSetting(SETTINGS.INVITE_ACKNOWLEDGED), + (currentPage, historyStack, firstRunCompleted, inviteAcknowledged) => { + // Determine if any links should show a tooltip for a guided tour + // It will only show one at a time, in the order they are set. + const guidedTourItem = [ + { + 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 getPreviousSubLinkPath = checkIfValidPage => { @@ -124,107 +141,92 @@ export const selectNavLinks = createSelector( // Gets the last active sublink in a section const getActiveSublink = category => { - if (category === 'wallet') { + if (category === PAGES.WALLET) { const previousPath = getPreviousSubLinkPath(isWalletPage); - return previousPath || '/wallet'; + return previousPath || `/${PAGES.WALLET}`; } 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 = [ { - label: 'Overview', - path: '/wallet', - active: currentPage === 'wallet', + ...buildLink('Overview', PAGES.WALLET), }, { - label: 'Send & Receive', - path: '/send', - active: currentPage === 'send', + ...buildLink('Send & Receive', PAGES.SEND), }, { - label: 'Transactions', - path: '/history', - active: currentPage === 'history', + ...buildLink('Transactions', PAGES.HISTORY), }, { - label: 'Get Credits', - path: '/getcredits', - active: currentPage === 'getcredits', + ...buildLink('Get Credits', PAGES.GET_CREDITS), }, { - label: 'Rewards', - path: '/rewards', - active: currentPage === 'rewards', + ...buildLink('Rewards', PAGES.REWARDS), }, { - label: 'Backup', - path: '/backup', - active: currentPage === 'backup', + ...buildLink('Backup', PAGES.BACKUP), }, ]; const navLinks = { primary: [ { - label: 'Explore', - path: '/discover', - active: currentPage === 'discover', - icon: icons.HOME, + ...buildLink('Explore', PAGES.DISCOVER), + icon: ICONS.HOME, }, { - label: 'Subscriptions', - path: '/subscriptions', - active: currentPage === 'subscriptions', - icon: icons.SUBSCRIPTION, + ...buildLink('Subscriptions', PAGES.SUBSCRIPTIONS), + icon: ICONS.SUBSCRIPTION, }, ], secondary: [ { label: 'Wallet', - icon: icons.WALLET, + icon: ICONS.WALLET, subLinks: walletSubLinks, - path: isCurrentlyWalletPage ? '/wallet' : getActiveSublink('wallet'), + path: isCurrentlyWalletPage ? `/${PAGES.WALLET}` : getActiveSublink(PAGES.WALLET), active: isWalletPage(currentPage), }, { - label: 'Invite', - icon: icons.INVITE, - path: '/invite', - active: currentPage === 'invite', + ...buildLink('Invite', PAGES.INVITE), + icon: ICONS.INVITE, }, { - label: 'Downloads', - icon: icons.LOCAL, - path: '/downloaded', - active: currentPage === 'downloaded', + ...buildLink('Downloads', PAGES.DOWNLOADED), + icon: ICONS.LOCAL, }, { - label: 'Publishes', - icon: icons.PUBLISHED, - path: '/published', - active: currentPage === 'published', + ...buildLink('Publishes', PAGES.PUBLISHED), + icon: ICONS.PUBLISHED, }, { - label: 'History', - icon: icons.HISTORY, - path: '/user_history', - active: currentPage === 'user_history', + ...buildLink('History', PAGES.USER_HISTORY), + icon: ICONS.HISTORY, }, { - label: 'Settings', - icon: icons.SETTINGS, - path: '/settings', - active: currentPage === 'settings', + ...buildLink('Settings', PAGES.SETTINGS), + icon: ICONS.SETTINGS, }, { - label: 'Help', - path: '/help', - icon: icons.HELP, - active: currentPage === 'help', + ...buildLink('Help', PAGES.HELP), + icon: ICONS.HELP, }, ], }; diff --git a/src/renderer/scss/component/_subscriptions.scss b/src/renderer/scss/component/_subscriptions.scss index 6dc9217b9..8ce693adc 100644 --- a/src/renderer/scss/component/_subscriptions.scss +++ b/src/renderer/scss/component/_subscriptions.scss @@ -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 added in more places with different layouts -.subscriptions__gerbil { -} - .subscriptions__suggested { animation: expand 0.2s; left: -2rem; position: relative; 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; - } -} diff --git a/src/renderer/scss/component/_tooltip.scss b/src/renderer/scss/component/_tooltip.scss index 4fa1a8081..f74935e91 100644 --- a/src/renderer/scss/component/_tooltip.scss +++ b/src/renderer/scss/component/_tooltip.scss @@ -1,11 +1,10 @@ .tooltip { display: inline-block; position: relative; + z-index: 2; - &:not(:hover) { - .tooltip__body { - visibility: hidden; - } + .tooltip__body { + visibility: hidden; } &:hover { @@ -15,17 +14,20 @@ } .tooltip__body { - background-color: $lbry-gray-5; - border-radius: 8px; - color: $lbry-white; font-size: 1rem; - font-weight: 500; + color: $lbry-black; + font-weight: 400; padding: var(--spacing-vertical-small); position: absolute; text-align: center; white-space: pre-wrap; 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 { width: 0; @@ -37,12 +39,18 @@ position: absolute; } - &--short { + &.tooltip__body--short { width: 130px; } } } +.tooltip--always-visible { + .tooltip__body { + visibility: visible; + } +} + .tooltip--bottom .tooltip__body { top: 90%; left: 50%; diff --git a/src/renderer/scss/component/_yrbl.scss b/src/renderer/scss/component/_yrbl.scss index 7dba455b1..09a232073 100644 --- a/src/renderer/scss/component/_yrbl.scss +++ b/src/renderer/scss/component/_yrbl.scss @@ -1,10 +1,19 @@ +.yrbl-wrap { + align-items: center; + display: flex; + justify-content: center; + vertical-align: middle; + margin-bottom: var(--spacing-vertical-large); +} + .yrbl { height: 300px; + margin-right: var(--spacing-vertical-large); } .yrbl--first-run { align-self: center; - height: 200px; + height: 250px; width: auto; margin: 0 var(--spacing-vertical-large); }