Refactor header Component, split into smaller components and remove what is unused

This commit is contained in:
Rafael 2021-12-20 09:29:51 -03:00 committed by Thomas Zarebczan
parent a196f7a3b0
commit 254fff208d
13 changed files with 370 additions and 588 deletions

View file

@ -0,0 +1,22 @@
// @flow
import React from 'react';
import { Link } from 'react-router-dom';
import { MenuLink } from '@reach/menu-button';
import Icon from 'component/common/icon';
type Props = {
icon: string,
name: string,
page: string,
};
export default function HeaderMenuLink(props: Props) {
const { icon, name, page } = props;
return (
<MenuLink className="menu__link" as={Link} to={`/$/${page}`}>
<Icon aria-hidden icon={icon} />
{name}
</MenuLink>
);
}

View file

@ -1,40 +1,33 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectTotalBalance, selectBalance } from 'redux/selectors/wallet';
import { formatCredits } from 'util/format-credits';
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
import { selectUserVerifiedEmail, selectUserEmail, selectEmailToVerify, selectUser } from 'redux/selectors/user';
import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user'; import { doClearEmailEntry, doClearPasswordEntry } from 'redux/actions/user';
import { doSetClientSetting } from 'redux/actions/settings';
import { doSignOut, doOpenModal } from 'redux/actions/app'; import { doSignOut, doOpenModal } from 'redux/actions/app';
import { selectClientSetting, selectLanguage } from 'redux/selectors/settings'; import { formatCredits } from 'util/format-credits';
import { selectHasNavigated, selectActiveChannelClaim, selectActiveChannelStakedLevel } from 'redux/selectors/app'; import { selectClientSetting } from 'redux/selectors/settings';
import { selectGetSyncErrorMessage } from 'redux/selectors/sync';
import { selectHasNavigated } from 'redux/selectors/app';
import { selectTotalBalance, selectBalance } from 'redux/selectors/wallet';
import { selectUserVerifiedEmail, selectEmailToVerify, selectUser } from 'redux/selectors/user';
import * as MODALS from 'constants/modal_types';
import * as SETTINGS from 'constants/settings';
import Header from './view'; import Header from './view';
const select = (state) => ({ const select = (state) => ({
language: selectLanguage(state),
balance: selectBalance(state),
roundedSpendableBalance: formatCredits(selectBalance(state), 2, true),
roundedBalance: formatCredits(selectTotalBalance(state), 2, true),
currentTheme: selectClientSetting(state, SETTINGS.THEME),
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
hideBalance: selectClientSetting(state, SETTINGS.HIDE_BALANCE),
authenticated: selectUserVerifiedEmail(state), authenticated: selectUserVerifiedEmail(state),
email: selectUserEmail(state), balance: selectBalance(state),
syncError: selectGetSyncErrorMessage(state),
emailToVerify: selectEmailToVerify(state), emailToVerify: selectEmailToVerify(state),
hasNavigated: selectHasNavigated(state), hasNavigated: selectHasNavigated(state),
hideBalance: selectClientSetting(state, SETTINGS.HIDE_BALANCE),
roundedBalance: formatCredits(selectTotalBalance(state), 2, true),
roundedSpendableBalance: formatCredits(selectBalance(state), 2, true),
syncError: selectGetSyncErrorMessage(state),
user: selectUser(state), user: selectUser(state),
activeChannelClaim: selectActiveChannelClaim(state),
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
}); });
const perform = (dispatch) => ({ const perform = (dispatch) => ({
setClientSetting: (key, value, push) => dispatch(doSetClientSetting(key, value, push)),
signOut: () => dispatch(doSignOut()),
doOpenModal: (modal, props) => dispatch(doOpenModal(modal, props)),
clearEmailEntry: () => dispatch(doClearEmailEntry()), clearEmailEntry: () => dispatch(doClearEmailEntry()),
clearPasswordEntry: () => dispatch(doClearPasswordEntry()), clearPasswordEntry: () => dispatch(doClearPasswordEntry()),
signOut: () => dispatch(doSignOut()),
openChangelog: (modalProps) => dispatch(doOpenModal(MODALS.CONFIRM, modalProps)),
}); });
export default connect(select, perform)(Header); export default connect(select, perform)(Header);

View file

@ -1,50 +1,19 @@
// @flow // @flow
import { ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM, ENABLE_UI_NOTIFICATIONS } from 'config';
import * as ICONS from 'constants/icons';
import * as MODALS from 'constants/modal_types';
import * as SETTINGS from 'constants/settings';
import * as PAGES from 'constants/pages';
import React from 'react';
import { withRouter } from 'react-router';
import { Link } from 'react-router-dom';
import classnames from 'classnames';
import Button from 'component/button';
import WunderBar from 'component/wunderbar';
import Icon from 'component/common/icon';
import { Menu, MenuList, MenuButton, MenuItem, MenuLink } from '@reach/menu-button';
import NavigationButton from 'component/navigationButton';
import { useIsMobile } from 'effects/use-screensize'; import { useIsMobile } from 'effects/use-screensize';
import NotificationBubble from 'component/notificationBubble'; import { withRouter } from 'react-router';
import NotificationHeaderButton from 'component/notificationHeaderButton'; import * as ICONS from 'constants/icons';
import ChannelThumbnail from 'component/channelThumbnail'; import * as PAGES from 'constants/pages';
import SkipNavigationButton from 'component/skipNavigationButton'; import Button from 'component/button';
import classnames from 'classnames';
import HeaderMenuButtons from 'component/headerMenuButtons';
import HeaderProfileMenuButton from 'component/headerProfileMenuButton';
import Logo from 'component/logo'; import Logo from 'component/logo';
// @if TARGET='app' import NotificationBubble from 'component/notificationBubble';
import { remote } from 'electron'; import React from 'react';
import { IS_MAC } from 'component/app/view'; import SkipNavigationButton from 'component/skipNavigationButton';
// @endif import WunderBar from 'component/wunderbar';
type Props = { type Props = {
user: ?User,
balance: string,
balance: number,
roundedBalance: string,
roundedSpendableBalance: string,
history: {
entities: {}[],
goBack: () => void,
goForward: () => void,
index: number,
length: number,
location: { pathname: string },
push: (string) => void,
replace: (string) => void,
},
currentTheme: string,
automaticDarkModeEnabled: boolean,
setClientSetting: (string, boolean | string, ?boolean) => void,
hideBalance: boolean,
email: ?string,
authenticated: boolean, authenticated: boolean,
authHeader: boolean, authHeader: boolean,
backout: { backout: {
@ -53,93 +22,82 @@ type Props = {
title: string, title: string,
simpleTitle: string, // Just use the same value as `title` if `title` is already short (~< 10 chars), unless you have a better idea for title overlfow on mobile simpleTitle: string, // Just use the same value as `title` if `title` is already short (~< 10 chars), unless you have a better idea for title overlfow on mobile
}, },
syncError: ?string, balance: number,
emailToVerify?: string, emailToVerify?: string,
signOut: () => void, hasNavigated: boolean,
doOpenModal: (string, ?{}) => void, hideBalance: boolean,
hideCancel: boolean,
history: {
goBack: () => void,
location: { pathname: string },
push: (string) => void,
replace: (string) => void,
},
isAbsoluteSideNavHidden: boolean,
roundedBalance: string,
roundedSpendableBalance: string,
sidebarOpen: boolean,
syncError: ?string,
user: ?User,
clearEmailEntry: () => void, clearEmailEntry: () => void,
clearPasswordEntry: () => void, clearPasswordEntry: () => void,
hasNavigated: boolean, openChangelog: ({}) => void,
sidebarOpen: boolean,
setSidebarOpen: (boolean) => void, setSidebarOpen: (boolean) => void,
isAbsoluteSideNavHidden: boolean, signOut: () => void,
hideCancel: boolean,
activeChannelClaim: ?ChannelClaim,
activeChannelStakedLevel: number,
}; };
const Header = (props: Props) => { const Header = (props: Props) => {
const { const {
balance,
roundedBalance,
roundedSpendableBalance,
history,
setClientSetting,
currentTheme,
automaticDarkModeEnabled,
hideBalance,
email,
authenticated, authenticated,
authHeader, authHeader,
signOut, backout,
balance,
emailToVerify,
hideBalance,
hideCancel,
history,
isAbsoluteSideNavHidden,
roundedBalance,
roundedSpendableBalance,
sidebarOpen,
syncError, syncError,
doOpenModal, user,
clearEmailEntry, clearEmailEntry,
clearPasswordEntry, clearPasswordEntry,
emailToVerify, openChangelog,
backout,
sidebarOpen,
setSidebarOpen, setSidebarOpen,
isAbsoluteSideNavHidden, signOut,
hideCancel,
user,
activeChannelClaim,
activeChannelStakedLevel,
} = props; } = props;
const {
location: { pathname },
goBack,
push,
} = history;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent // on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
const isVerifyPage = history.location.pathname.includes(PAGES.AUTH_VERIFY); const isVerifyPage = pathname.includes(PAGES.AUTH_VERIFY);
const isSignUpPage = history.location.pathname.includes(PAGES.AUTH); const isSignUpPage = pathname.includes(PAGES.AUTH);
const isSignInPage = history.location.pathname.includes(PAGES.AUTH_SIGNIN); const isSignInPage = pathname.includes(PAGES.AUTH_SIGNIN);
const isPwdResetPage = history.location.pathname.includes(PAGES.AUTH_PASSWORD_RESET); const isPwdResetPage = pathname.includes(PAGES.AUTH_PASSWORD_RESET);
const hasBackout = Boolean(backout);
// For pages that allow for "backing out", shows a backout option instead of the Home logo
const canBackout = Boolean(backout);
const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {}; const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {};
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
const livestreamEnabled = Boolean(
ENABLE_NO_SOURCE_CLAIMS &&
user &&
!user.odysee_live_disabled &&
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled)
);
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
// Sign out if they click the "x" when they are on the password prompt // Sign out if they click the "x" when they are on the password prompt
const authHeaderAction = syncError ? { onClick: signOut } : { navigate: '/' }; const authHeaderAction = syncError && { onClick: signOut };
const homeButtonNavigationProps = isVerifyPage ? {} : authHeader ? authHeaderAction : { navigate: '/' }; const homeButtonNavigationProps = (isVerifyPage && {}) || (authHeader && authHeaderAction) || { navigate: '/' };
const closeButtonNavigationProps = { const sidebarLabel = sidebarOpen
onClick: () => { ? __('Close sidebar - hide channels you are following.')
clearEmailEntry(); : __('Expand sidebar - view channels you are following.');
clearPasswordEntry();
if (syncError) { const onBackout = React.useCallback(
signOut(); (e: any) => {
} const { hasNavigated } = props;
const { replace } = history;
if (isSignInPage && !emailToVerify) {
history.goBack();
} else if (isSignUpPage) {
history.goBack();
} else if (isPwdResetPage) {
history.goBack();
} else {
history.push('/');
}
},
};
function onBackout(e) {
const { history, hasNavigated } = props;
const { goBack, replace } = history;
window.removeEventListener('popstate', onBackout); window.removeEventListener('popstate', onBackout);
@ -151,28 +109,36 @@ const Header = (props: Props) => {
replace(backNavDefault || `/`); replace(backNavDefault || `/`);
} }
} }
} },
[backNavDefault, goBack, history, props]
);
React.useEffect(() => { React.useEffect(() => {
if (hasBackout) { if (canBackout) {
window.addEventListener('popstate', onBackout); window.addEventListener('popstate', onBackout);
return () => window.removeEventListener('popstate', onBackout); return () => window.removeEventListener('popstate', onBackout);
} }
}, [hasBackout]); }, [canBackout, onBackout]);
function handleThemeToggle() { const userButtons = (className: string) => (
if (automaticDarkModeEnabled) { <div className={classnames('header__menu', { 'header__menu--with-balance': authenticated })}>
setClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false); {authenticated ? (
<>
<Button
title={
balance > 0
? __('Immediately spendable: %spendable_balance%', { spendable_balance: roundedSpendableBalance })
: __('Your Wallet')
} }
navigate={`/$/${PAGES.WALLET}`}
className={classnames(className, 'header__navigation-item--balance')}
label={hideBalance || Number(roundedBalance) === 0 ? __('Your Wallet') : roundedBalance}
icon={ICONS.LBC}
/>
if (currentTheme === 'dark') { <HeaderProfileMenuButton />
setClientSetting(SETTINGS.THEME, 'light', true); </>
} else { ) : (
setClientSetting(SETTINGS.THEME, 'dark', true);
}
}
const loginButtons = (
<div className="header__auth-buttons"> <div className="header__auth-buttons">
<Button <Button
navigate={`/$/${PAGES.AUTH_SIGNIN}`} navigate={`/$/${PAGES.AUTH_SIGNIN}`}
@ -183,90 +149,44 @@ const Header = (props: Props) => {
/> />
<Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Sign Up')} disabled={user === null} /> <Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Sign Up')} disabled={user === null} />
</div> </div>
); )}
</div>
type BalanceButtonProps = { className: string };
const BalanceButton = (balanceButtonProps: BalanceButtonProps) => (
<Button
title={
balance > 0
? __('Immediately spendable: %spendable_balance%', { spendable_balance: roundedSpendableBalance })
: __('Your Wallet')
}
navigate={`/$/${PAGES.WALLET}`}
className={classnames(balanceButtonProps.className, 'header__navigation-item--balance')}
label={hideBalance || Number(roundedBalance) === 0 ? __('Your Wallet') : roundedBalance}
icon={ICONS.LBC}
// @if TARGET='app'
onDoubleClick={(e) => {
e.stopPropagation();
}}
// @endif
/>
); );
return ( return (
<header <header className={classnames('header', { 'header--minimal': authHeader })}>
className={classnames('header', {
'header--minimal': authHeader,
// @if TARGET='app'
'header--mac': IS_MAC,
// @endif
})}
// @if TARGET='app'
onDoubleClick={(e) => {
remote.getCurrentWindow().maximize();
}}
// @endif
>
<div className="header__contents"> <div className="header__contents">
{!authHeader && backout ? ( {!authHeader && canBackout ? (
<div className="card__actions--between"> <div className="card__actions--between">
<Button <Button onClick={onBackout} button="link" label={backLabel || __('Cancel')} icon={ICONS.ARROW_LEFT} />
onClick={onBackout}
button="link" {backTitle && <h1 className="header__auth-title">{(isMobile && simpleBackTitle) || backTitle}</h1>}
label={(backLabel && backLabel) || __('Cancel')}
icon={ICONS.ARROW_LEFT} {userButtons('header__navigation-item menu__title')}
/>
{backTitle && <h1 className="header__auth-title">{isMobile ? simpleBackTitle || backTitle : backTitle}</h1>}
{authenticated || !IS_WEB ? (
<BalanceButton className="header__navigation-item menu__title" />
) : (
loginButtons
)}
</div> </div>
) : ( ) : (
<> <>
<div className="header__navigation"> <div className="header__navigation">
<SkipNavigationButton /> <SkipNavigationButton />
{!authHeader && ( {!authHeader && (
<span style={{ position: 'relative' }}> <span style={{ position: 'relative' }}>
<Button <Button
aria-label={ aria-label={sidebarLabel}
sidebarOpen
? __('Close sidebar - hide channels you are following.')
: __('Expand sidebar - view channels you are following.')
}
className="header__navigation-item menu__title header__navigation-item--icon" className="header__navigation-item menu__title header__navigation-item--icon"
icon={ICONS.MENU} icon={ICONS.MENU}
aria-expanded={sidebarOpen} aria-expanded={sidebarOpen}
onClick={() => setSidebarOpen(!sidebarOpen)} onClick={() => setSidebarOpen(!sidebarOpen)}
> >
{isAbsoluteSideNavHidden && isMobile && notificationsEnabled && <NotificationBubble />} {isAbsoluteSideNavHidden && isMobile && <NotificationBubble />}
</Button> </Button>
</span> </span>
)} )}
<Button <Button
aria-label={__('Home')} aria-label={__('Home')}
className="header__navigation-item header__navigation-item--lbry" className="header__navigation-item header__navigation-item--lbry"
onClick={() => { onClick={() => pathname === '/' && window.location.reload()}
if (history.location.pathname === '/') window.location.reload();
}}
// @if TARGET='app'
onDoubleClick={(e) => {
e.stopPropagation();
}}
// @endif
{...homeButtonNavigationProps} {...homeButtonNavigationProps}
> >
<Logo /> <Logo />
@ -275,125 +195,34 @@ const Header = (props: Props) => {
{/* @if process.env.DEV_CHANGELOG */} {/* @if process.env.DEV_CHANGELOG */}
{history.location.pathname === '/' && ( {history.location.pathname === '/' && (
<Button <Button
title={'Changelog'} title="Changelog"
className="badge--alert" className="badge--alert"
label={'Changelog'} label="Changelog"
icon={ICONS.FEEDBACK} icon={ICONS.FEEDBACK}
onClick={() => { onClick={() =>
doOpenModal(MODALS.CONFIRM, { openChangelog({
title: __('Changelog'), title: __('Changelog'),
subtitle: __('Warning: this is a test instance.'), subtitle: __('Warning: this is a test instance.'),
body: <p style={{ whiteSpace: 'pre-wrap' }}>{process.env.DEV_CHANGELOG}</p>, body: <p style={{ whiteSpace: 'pre-wrap' }}>{process.env.DEV_CHANGELOG}</p>,
onConfirm: (closeModal) => closeModal(), onConfirm: (closeModal) => closeModal(),
hideCancel: true, hideCancel: true,
}); })
}} }
/> />
)} )}
{/* @endif */} {/* @endif */}
{!authHeader && ( {!authHeader && (
<div className="header__center"> <div className="header__center">
{/* @if TARGET='app' */} <WunderBar />
{!authHeader && ( <HeaderMenuButtons />
<div className="header__buttons">
<NavigationButton isBackward history={history} />
<NavigationButton isBackward={false} history={history} />
</div>
)}
{/* @endif */}
{!authHeader && <WunderBar />}
<HeaderMenuButtons
authenticated={authenticated}
notificationsEnabled={notificationsEnabled}
history={history}
handleThemeToggle={handleThemeToggle}
currentTheme={currentTheme}
livestreamEnabled={livestreamEnabled}
/>
</div> </div>
)} )}
</div> </div>
{!authHeader && !backout ? ( {!authHeader && !canBackout
<div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}> ? userButtons('header__navigation-item menu__title mobile-hidden')
{(!IS_WEB || authenticated) && ( : !isVerifyPage &&
<BalanceButton className="header__navigation-item menu__title mobile-hidden" />
)}
{IS_WEB && !authenticated && loginButtons}
{(authenticated || !IS_WEB) && (
<Menu>
<MenuButton
aria-label={__('Your account')}
title={__('Your account')}
className={classnames('header__navigation-item', {
'menu__title header__navigation-item--icon': !activeChannelUrl,
'header__navigation-item--profile-pic': activeChannelUrl,
})}
// @if TARGET='app'
onDoubleClick={(e) => {
e.stopPropagation();
}}
// @endif
>
{activeChannelUrl ? (
<ChannelThumbnail uri={activeChannelUrl} small noLazyLoad />
) : (
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
)}
</MenuButton>
<MenuList className="menu__list--header">
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.UPLOADS}`}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Uploads')}
</MenuLink>
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.CHANNELS}`}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('Channels')}
</MenuLink>
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.CREATOR_DASHBOARD}`}>
<Icon aria-hidden icon={ICONS.ANALYTICS} />
{__('Creator Analytics')}
</MenuLink>
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.REWARDS}`}>
<Icon aria-hidden icon={ICONS.REWARDS} />
{__('Rewards')}
</MenuLink>
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.INVITE}`}>
<Icon aria-hidden icon={ICONS.INVITE} />
{__('Invites')}
</MenuLink>
{authenticated ? (
<MenuItem onSelect={IS_WEB ? signOut : () => doOpenModal(MODALS.SIGN_OUT)}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
</div>
<span className="menu__link-help">{email}</span>
</MenuItem>
) : !IS_WEB ? (
<>
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.AUTH}`}>
<Icon aria-hidden icon={ICONS.SIGN_UP} />
{__('Sign Up')}
</MenuLink>
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.AUTH_SIGNIN}`}>
<Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')}
</MenuLink>
</>
) : null}
</MenuList>
</Menu>
)}
</div>
) : (
!isVerifyPage &&
!hideCancel && ( !hideCancel && (
<div className="header__menu"> <div className="header__menu">
{/* Add an empty span here so we can use the same style as above */} {/* Add an empty span here so we can use the same style as above */}
@ -405,15 +234,20 @@ const Header = (props: Props) => {
button="alt" button="alt"
// className="button--header-close" // className="button--header-close"
icon={ICONS.REMOVE} icon={ICONS.REMOVE}
{...closeButtonNavigationProps} onClick={() => {
// @if TARGET='app' clearEmailEntry();
onDoubleClick={(e) => { clearPasswordEntry();
e.stopPropagation();
if (syncError) signOut();
if ((isSignInPage && !emailToVerify) || isSignUpPage || isPwdResetPage) {
goBack();
} else {
push('/');
}
}} }}
// @endif
/> />
</div> </div>
)
)} )}
</> </>
)} )}
@ -422,92 +256,4 @@ const Header = (props: Props) => {
); );
}; };
type HeaderMenuButtonProps = {
authenticated: boolean,
notificationsEnabled: boolean,
history: { push: (string) => void },
handleThemeToggle: (string) => void,
currentTheme: string,
livestreamEnabled: boolean,
};
function HeaderMenuButtons(props: HeaderMenuButtonProps) {
const { authenticated, notificationsEnabled, handleThemeToggle, currentTheme, livestreamEnabled } = props;
return (
<div className="header__buttons">
{(authenticated || !IS_WEB) && (
<Menu>
<MenuButton
aria-label={__('Publish a file, or create a channel')}
title={__('Publish a file, or create a channel')}
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
// @if TARGET='app'
onDoubleClick={(e) => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.PUBLISH} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.UPLOAD}`}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Upload')}
</MenuLink>
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.CHANNEL_NEW}`}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('New Channel')}
</MenuLink>
{/* @if TARGET='web' */}
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.YOUTUBE_SYNC}`}>
<Icon aria-hidden icon={ICONS.YOUTUBE} />
{__('Sync YouTube Channel')}
</MenuLink>
{/* @endif */}
{livestreamEnabled && (
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.LIVESTREAM}`}>
<Icon aria-hidden icon={ICONS.VIDEO} />
{__('Go Live')}
</MenuLink>
)}
</MenuList>
</Menu>
)}
{notificationsEnabled && <NotificationHeaderButton />}
<Menu>
<MenuButton
aria-label={__('Settings')}
title={__('Settings')}
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
// @if TARGET='app'
onDoubleClick={(e) => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.SETTINGS} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.SETTINGS}`}>
<Icon aria-hidden tooltip icon={ICONS.SETTINGS} />
{__('Settings')}
</MenuLink>
<MenuLink className="menu__link" as={Link} to={`/$/${PAGES.HELP}`}>
<Icon aria-hidden icon={ICONS.HELP} />
{__('Help')}
</MenuLink>
<MenuItem className="menu__link" onSelect={handleThemeToggle}>
<Icon icon={currentTheme === 'light' ? ICONS.DARK : ICONS.LIGHT} />
{currentTheme === 'light' ? __('Dark') : __('Light')}
</MenuItem>
</MenuList>
</Menu>
</div>
);
}
export default withRouter(Header); export default withRouter(Header);

View file

@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import { doSetClientSetting } from 'redux/actions/settings';
import { selectActiveChannelStakedLevel } from 'redux/selectors/app';
import { selectClientSetting } from 'redux/selectors/settings';
import * as SETTINGS from 'constants/settings';
import HeaderMenuButtons from './view';
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
const select = (state) => ({
activeChannelStakedLevel: selectActiveChannelStakedLevel(state),
authenticated: selectUserVerifiedEmail(state),
automaticDarkModeEnabled: selectClientSetting(state, SETTINGS.AUTOMATIC_DARK_MODE_ENABLED),
currentTheme: selectClientSetting(state, SETTINGS.THEME),
user: selectUser(state),
});
const perform = (dispatch) => ({
handleThemeToggle: (automaticDarkModeEnabled, currentTheme) => {
if (automaticDarkModeEnabled) dispatch(doSetClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false));
dispatch(doSetClientSetting(SETTINGS.THEME, currentTheme === 'dark' ? 'light' : 'dark', true));
},
});
export default connect(select, perform)(HeaderMenuButtons);

View file

@ -0,0 +1,82 @@
// @flow
import { ENABLE_UI_NOTIFICATIONS, ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import HeaderMenuLink from 'component/common/header-menu-link';
import Icon from 'component/common/icon';
import NotificationHeaderButton from 'component/notificationHeaderButton';
import React from 'react';
type HeaderMenuButtonProps = {
activeChannelStakedLevel: number,
authenticated: boolean,
automaticDarkModeEnabled: boolean,
currentTheme: string,
user: ?User,
handleThemeToggle: (boolean, string) => void,
};
export default function HeaderMenuButtons(props: HeaderMenuButtonProps) {
const {
authenticated,
automaticDarkModeEnabled,
currentTheme,
activeChannelStakedLevel,
user,
handleThemeToggle,
} = props;
const notificationsEnabled = ENABLE_UI_NOTIFICATIONS || (user && user.experimental_ui);
const livestreamEnabled = Boolean(
ENABLE_NO_SOURCE_CLAIMS &&
user &&
!user.odysee_live_disabled &&
(activeChannelStakedLevel >= CHANNEL_STAKED_LEVEL_LIVESTREAM || user.odysee_live_enabled)
);
return (
<div className="header__buttons">
{authenticated && (
<Menu>
<MenuButton
aria-label={__('Publish a file, or create a channel')}
title={__('Publish a file, or create a channel')}
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
>
<Icon size={18} icon={ICONS.PUBLISH} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<HeaderMenuLink page={PAGES.UPLOAD} icon={ICONS.PUBLISH} name={__('Upload')} />
<HeaderMenuLink page={PAGES.CHANNEL_NEW} icon={ICONS.CHANNEL} name={__('New Channel')} />
<HeaderMenuLink page={PAGES.YOUTUBE_SYNC} icon={ICONS.YOUTUBE} name={__('Sync YouTube Channel')} />
{livestreamEnabled && <HeaderMenuLink page={PAGES.LIVESTREAM} icon={ICONS.VIDEO} name={__('Go Live')} />}
</MenuList>
</Menu>
)}
{notificationsEnabled && <NotificationHeaderButton />}
<Menu>
<MenuButton
aria-label={__('Settings')}
title={__('Settings')}
className="header__navigation-item menu__title header__navigation-item--icon mobile-hidden"
>
<Icon size={18} icon={ICONS.SETTINGS} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<HeaderMenuLink page={PAGES.SETTINGS} icon={ICONS.SETTINGS} name={__('Settings')} />
<HeaderMenuLink page={PAGES.HELP} icon={ICONS.HELP} name={__('Help')} />
<MenuItem className="menu__link" onSelect={() => handleThemeToggle(automaticDarkModeEnabled, currentTheme)}>
<Icon icon={currentTheme === 'light' ? ICONS.DARK : ICONS.LIGHT} />
{currentTheme === 'light' ? __('Dark') : __('Light')}
</MenuItem>
</MenuList>
</Menu>
</div>
);
}

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { doSignOut } from 'redux/actions/app';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { selectUserEmail } from 'redux/selectors/user';
import HeaderProfileMenuButton from './view';
const select = (state) => ({
activeChannelClaim: selectActiveChannelClaim(state),
email: selectUserEmail(state),
});
const perform = (dispatch) => ({
signOut: () => dispatch(doSignOut()),
});
export default connect(select, perform)(HeaderProfileMenuButton);

View file

@ -0,0 +1,58 @@
// @flow
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages';
import ChannelThumbnail from 'component/channelThumbnail';
import classnames from 'classnames';
import HeaderMenuLink from 'component/common/header-menu-link';
import Icon from 'component/common/icon';
import React from 'react';
type HeaderMenuButtonProps = {
activeChannelClaim: ?ChannelClaim,
email: ?string,
signOut: () => void,
};
export default function HeaderProfileMenuButton(props: HeaderMenuButtonProps) {
const { activeChannelClaim, email, signOut } = props;
const activeChannelUrl = activeChannelClaim && activeChannelClaim.permanent_url;
return (
<div className="header__buttons">
<Menu>
<MenuButton
aria-label={__('Your account')}
title={__('Your account')}
className={classnames('header__navigation-item', {
'menu__title header__navigation-item--icon': !activeChannelUrl,
'header__navigation-item--profile-pic': activeChannelUrl,
})}
>
{activeChannelUrl ? (
<ChannelThumbnail uri={activeChannelUrl} small noLazyLoad />
) : (
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
)}
</MenuButton>
<MenuList className="menu__list--header">
<HeaderMenuLink page={PAGES.UPLOADS} icon={ICONS.PUBLISH} name={__('Uploads')} />
<HeaderMenuLink page={PAGES.CHANNELS} icon={ICONS.CHANNEL} name={__('Channels')} />
<HeaderMenuLink page={PAGES.CREATOR_DASHBOARD} icon={ICONS.ANALYTICS} name={__('Creator Analytics')} />
<HeaderMenuLink page={PAGES.REWARDS} icon={ICONS.REWARDS} name={__('Rewards')} />
<HeaderMenuLink page={PAGES.INVITE} icon={ICONS.INVITE} name={__('Invites')} />
<MenuItem onSelect={signOut}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
</div>
<span className="menu__link-help">{email}</span>
</MenuItem>
</MenuList>
</Menu>
</div>
);
}

View file

@ -1,3 +0,0 @@
import NavigationButton from './view';
export default NavigationButton;

View file

@ -1,97 +0,0 @@
// @flow
import React, { useState, useCallback } from 'react';
import * as ICONS from 'constants/icons';
import Button from 'component/button';
// the maximum length of history to show per button
const MAX_HISTORY_SIZE = 12;
type Props = {
isBackward: boolean,
history: {
entries: Array<{ key: string, title: string, pathname: string }>,
go: number => void,
goBack: () => void,
goForward: () => void,
index: number,
length: number,
location: { pathname: string },
push: string => void,
},
};
// determines which slice of entries should make up the back or forward button drop-downs (isBackward vs !isBackward respectively)
const sliceEntries = (currentIndex, entries, historyLength, isBackward, maxSize) => {
let l = isBackward ? 0 : currentIndex + 1;
let r = isBackward ? currentIndex : historyLength;
const exceedsMax = maxSize < r - l;
if (!exceedsMax) {
return entries.slice(l, r);
} else if (isBackward) {
l = r - maxSize;
} else {
r = l + maxSize;
}
return entries.slice(l, r);
};
const NavigationButton = (props: Props) => {
const { isBackward, history } = props;
const { entries, go } = history;
const currentIndex = history.index;
const historyLength = history.length;
const [showHistory, setShowHistory] = useState(false);
// creates an <li> intended for the button's <ul>
const makeItem = useCallback(
(entry: { pathname: string, title: string, key: string }, index: number) => {
// difference between the current index and the index of the entry
const backwardDif = index - (currentIndex < MAX_HISTORY_SIZE ? currentIndex : MAX_HISTORY_SIZE);
const forwardDif = index + 1;
return (
<li
className="header__navigation-button"
role="link"
key={entry.key}
onMouseDown={() => {
setShowHistory(false);
go(isBackward ? backwardDif : forwardDif);
}}
>
<span>{entry.title}</span>
<span className="header__navigation-button-help">{entry.pathname === '/' ? __('Home') : entry.pathname}</span>
</li>
);
},
[currentIndex, isBackward, go]
);
const slicedEntries = sliceEntries(currentIndex, entries, historyLength, isBackward, MAX_HISTORY_SIZE);
return (
<div
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Button
className={`header__navigation-item header__navigation-item--${isBackward ? 'back' : 'forward'}`}
description={isBackward ? __('Navigate back') : __('Navigate forward')}
onBlur={() => setShowHistory(false)}
onClick={() => (isBackward ? history.goBack() : history.goForward())}
onContextMenu={e => {
setShowHistory(!showHistory);
// the following three lines prevent the regular context menu (right click menu) from appearing
e.preventDefault();
e.stopPropagation();
return false;
}}
icon={isBackward ? ICONS.ARROW_LEFT : ICONS.ARROW_RIGHT}
iconSize={18}
disabled={slicedEntries.length === 0}
/>
{showHistory && <ul className={'header__navigation-dropdown'}>{slicedEntries.map(makeItem)}</ul>}
</div>
);
};
export default NavigationButton;

View file

@ -35,7 +35,6 @@ export const WALLET_PASSWORD_UNSAVE = 'wallet_password_unsave';
export const CREATE_CHANNEL = 'create_channel'; export const CREATE_CHANNEL = 'create_channel';
export const YOUTUBE_WELCOME = 'youtube_welcome'; export const YOUTUBE_WELCOME = 'youtube_welcome';
export const SET_REFERRER = 'set_referrer'; export const SET_REFERRER = 'set_referrer';
export const SIGN_OUT = 'sign_out';
export const LIQUIDATE_SUPPORTS = 'liquidate_supports'; export const LIQUIDATE_SUPPORTS = 'liquidate_supports';
export const MASS_TIP_UNLOCK = 'mass_tip_unlock'; export const MASS_TIP_UNLOCK = 'mass_tip_unlock';
export const CONFIRM_AGE = 'confirm_age'; export const CONFIRM_AGE = 'confirm_age';

View file

@ -73,7 +73,6 @@ const ModalRevokeClaim = lazyImport(() => import('modal/modalRevokeClaim' /* web
const ModalRewardCode = lazyImport(() => import('modal/modalRewardCode' /* webpackChunkName: "modalRewardCode" */)); const ModalRewardCode = lazyImport(() => import('modal/modalRewardCode' /* webpackChunkName: "modalRewardCode" */));
const ModalSendTip = lazyImport(() => import('modal/modalSendTip' /* webpackChunkName: "modalSendTip" */)); const ModalSendTip = lazyImport(() => import('modal/modalSendTip' /* webpackChunkName: "modalSendTip" */));
const ModalSetReferrer = lazyImport(() => import('modal/modalSetReferrer' /* webpackChunkName: "modalSetReferrer" */)); const ModalSetReferrer = lazyImport(() => import('modal/modalSetReferrer' /* webpackChunkName: "modalSetReferrer" */));
const ModalSignOut = lazyImport(() => import('modal/modalSignOut' /* webpackChunkName: "modalSignOut" */));
const ModalSocialShare = lazyImport(() => import('modal/modalSocialShare' /* webpackChunkName: "modalSocialShare" */)); const ModalSocialShare = lazyImport(() => import('modal/modalSocialShare' /* webpackChunkName: "modalSocialShare" */));
const ModalSupportsLiquidate = lazyImport(() => const ModalSupportsLiquidate = lazyImport(() =>
import('modal/modalSupportsLiquidate' /* webpackChunkName: "modalSupportsLiquidate" */) import('modal/modalSupportsLiquidate' /* webpackChunkName: "modalSupportsLiquidate" */)
@ -157,8 +156,6 @@ function getModal(id) {
return ModalYoutubeWelcome; return ModalYoutubeWelcome;
case MODALS.SET_REFERRER: case MODALS.SET_REFERRER:
return ModalSetReferrer; return ModalSetReferrer;
case MODALS.SIGN_OUT:
return ModalSignOut;
case MODALS.CONFIRM_AGE: case MODALS.CONFIRM_AGE:
return ModalConfirmAge; return ModalConfirmAge;
case MODALS.FILE_SELECTION: case MODALS.FILE_SELECTION:

View file

@ -1,8 +0,0 @@
import { connect } from 'react-redux';
import { doSignOut, doHideModal } from 'redux/actions/app';
import ModalSignOut from './view';
export default connect(null, {
doSignOut,
doHideModal,
})(ModalSignOut);

View file

@ -1,47 +0,0 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import Card from 'component/common/card';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
type Props = {
doHideModal: () => void,
doSignOut: () => void,
};
function ModalRepost(props: Props) {
const { doHideModal, doSignOut } = props;
return (
<Modal isOpen type="card">
<Card
title={__('Sign out')}
subtitle={
<I18nMessage
tokens={{
rename_wallet_instructions: (
<Button
button="link"
label={__('rename your existing wallet')}
href="https://lbry.com/faq/lbry-directories"
/>
),
}}
>
Your wallet data will remain intact. If you sign in with a different account, the wallets will be merged. To
prevent this, you need to %rename_wallet_instructions% in the lbry/wallets directory.
</I18nMessage>
}
actions={
<div className="section__actions">
<Button button="primary" label={__('Sign Out')} onClick={doSignOut} />
<Button button="link" label={__('Cancel')} onClick={doHideModal} />
</div>
}
/>
</Modal>
);
}
export default ModalRepost;