From 5e118e04246d944a9bbd896e3a2bc1dcf870e3d0 Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Wed, 23 Jan 2019 22:21:53 -0500 Subject: [PATCH] feat: show guided tooltip to invite page on first run --- src/renderer/component/common/tooltip.jsx | 7 +- src/renderer/component/inviteList/view.jsx | 87 +++++++----- 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/index.js | 2 +- src/renderer/page/invite/index.js | 5 + src/renderer/page/invite/view.jsx | 20 ++- .../page/subscriptions/internal/first-run.jsx | 23 +-- src/renderer/redux/reducers/settings.js | 1 + src/renderer/redux/selectors/app.js | 127 ++++++++--------- src/renderer/scss/component/_tooltip.scss | 31 ++-- 13 files changed, 274 insertions(+), 187 deletions(-) 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 5d5aacadcae..303897fb4d3 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 + guided?: 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, guided } = 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--guided-help': guided, })} > {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/inviteList/view.jsx b/src/renderer/component/inviteList/view.jsx index 5dc1208928d..5ee18c5ddd4 100644 --- a/src/renderer/component/inviteList/view.jsx +++ b/src/renderer/component/inviteList/view.jsx @@ -1,6 +1,7 @@ // @flow import * as ICONS from 'constants/icons'; import React from 'react'; +import Native from 'native'; import Icon from 'component/common/icon'; import RewardLink from 'component/rewardLink'; import { rewards } from 'lbryinc'; @@ -22,6 +23,26 @@ class InviteList extends React.PureComponent { return null; } + if (!invitees.length) { + return ( +
+ Friendly gerbil +
+

{__('Some Invite Title')}

+

+ {__( + 'LBRY is powered by the users, not some large company. Invite your friends here to help out.' + )} +

+
+
+ ); + } + return (
@@ -29,43 +50,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/router/index.js b/src/renderer/component/router/index.js index ae525c4dd5f..c0919a6d48b 100644 --- a/src/renderer/component/router/index.js +++ b/src/renderer/component/router/index.js @@ -1,5 +1,5 @@ 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 Router from './view'; @@ -10,5 +10,5 @@ const select = state => ({ 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 879bb082dfb..030477e6ebd 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 00000000000..2ef273a2979 --- /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 f6336810570..58c93907e7e 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 INVITE_ACKNOWLEDGED = 'invite_acknowledged'; export const LANGUAGE = 'language'; export const SHOW_NSFW = 'showNsfw'; export const SHOW_UNAVAILABLE = 'showUnavailable'; diff --git a/src/renderer/index.js b/src/renderer/index.js index 98f9a40c2d0..ae0c9c987ab 100644 --- a/src/renderer/index.js +++ b/src/renderer/index.js @@ -221,7 +221,7 @@ const init = () => { app.store.dispatch(doAuthenticate(pjson.version))} - onReadyToLaunch={onDaemonReady} + onReadyToLaunch={console.log('ready to launch') || onDaemonReady} /> , document.getElementById('app') diff --git a/src/renderer/page/invite/index.js b/src/renderer/page/invite/index.js index c69a5269716..061bb67c1bb 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 04aadbe466e..550569eb5f7 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 a5999f42df0..12b2f330feb 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/redux/reducers/settings.js b/src/renderer/redux/reducers/settings.js index fd9c8c12d3f..536ba73fc8f 100644 --- a/src/renderer/redux/reducers/settings.js +++ b/src/renderer/redux/reducers/settings.js @@ -19,6 +19,7 @@ const defaultState = { showUnavailable: getLocalStorageSetting(SETTINGS.SHOW_UNAVAILABLE, true), welcome_acknowledged: getLocalStorageSetting(SETTINGS.NEW_USER_ACKNOWLEDGED, false), email_collection_acknowledged: getLocalStorageSetting(SETTINGS.EMAIL_COLLECTION_ACKNOWLEDGED), + invite_acknowledged: getLocalStorageSetting(SETTINGS.INVITE_ACKNOWLEDGED, false), credit_required_acknowledged: false, // this needs to be re-acknowledged every run language: getLocalStorageSetting(SETTINGS.LANGUAGE, 'en'), theme: getLocalStorageSetting(SETTINGS.THEME, 'light'), diff --git a/src/renderer/redux/selectors/app.js b/src/renderer/redux/selectors/app.js index 61a7da527ec..6dd11ad2853 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,28 @@ export const selectUpgradeTimer = createSelector(selectState, state => state.che export const selectNavLinks = createSelector( selectCurrentPage, selectHistoryStack, - (currentPage, historyStack) => { + makeSelectClientSetting(SETTINGS.INVITE_ACKNOWLEDGED), + (currentPage, historyStack, 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 === 'wallet' || - page === 'send' || - page === 'getcredits' || - page === 'rewards' || - page === 'history' || - page === 'backup'; + 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 +140,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 (!hasCompletedFirstRun) { + // return null; + // } + // something with this + 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/_tooltip.scss b/src/renderer/scss/component/_tooltip.scss index 4fa1a8081da..b5ee5b8c1b1 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,21 @@ } .tooltip__body { - background-color: $lbry-gray-5; - border-radius: 8px; - color: $lbry-white; - font-size: 1rem; - font-weight: 500; + // border-radius: 8px; + // font-size: 1rem; + 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'] & { + color: $lbry-black; + border: 1px solid $lbry-black; + background-color: $lbry-gray-1; + } &::after { width: 0; @@ -37,12 +40,18 @@ position: absolute; } - &--short { + &.tooltip__body--short { width: 130px; } } } +.tooltip--guided-help { + .tooltip__body { + visibility: visible; + } +} + .tooltip--bottom .tooltip__body { top: 90%; left: 50%;