lbry-desktop/ui/component/header/view.jsx

488 lines
17 KiB
React
Raw Normal View History

2018-03-26 23:32:43 +02:00
// @flow
import { LOGO_TITLE, ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
2018-11-26 02:21:25 +01:00
import * as ICONS from 'constants/icons';
import { SETTINGS } from 'lbry-redux';
2019-08-21 22:54:44 +02:00
import * as PAGES from 'constants/pages';
2020-08-11 22:32:03 +02:00
import React from 'react';
2019-06-17 22:32:38 +02:00
import { withRouter } from 'react-router';
2019-08-27 16:43:42 +02:00
import classnames from 'classnames';
2018-03-26 23:32:43 +02:00
import Button from 'component/button';
import WunderBar from 'component/wunderbar';
2019-06-17 22:32:38 +02:00
import Icon from 'component/common/icon';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import NavigationButton from 'component/navigationButton';
2020-08-10 22:47:39 +02:00
import { useIsMobile } from 'effects/use-screensize';
import NotificationBubble from 'component/notificationBubble';
import NotificationHeaderButton from 'component/notificationHeaderButton';
2020-10-05 19:38:40 +02:00
import ChannelThumbnail from 'component/channelThumbnail';
// @if TARGET='app'
import { remote } from 'electron';
import { IS_MAC } from 'component/app/view';
// @endif
2019-10-09 18:34:18 +02:00
2018-03-26 23:32:43 +02:00
type Props = {
2020-09-29 22:15:35 +02:00
user: ?User,
2018-03-26 23:32:43 +02:00
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,
},
2019-06-17 22:32:38 +02:00
currentTheme: string,
automaticDarkModeEnabled: boolean,
2020-09-04 17:02:30 +02:00
setClientSetting: (string, boolean | string, ?boolean) => void,
hideBalance: boolean,
2019-08-21 22:54:44 +02:00
email: ?string,
authenticated: boolean,
2019-10-28 15:04:37 +01:00
authHeader: boolean,
backout: {
backLabel?: string,
backNavDefault?: 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
},
syncError: ?string,
emailToVerify?: string,
2019-09-26 18:07:11 +02:00
signOut: () => void,
openSignOutModal: () => void,
clearEmailEntry: () => void,
clearPasswordEntry: () => void,
hasNavigated: boolean,
2020-08-10 22:47:39 +02:00
sidebarOpen: boolean,
setSidebarOpen: (boolean) => void,
2020-08-10 22:47:39 +02:00
isAbsoluteSideNavHidden: boolean,
2020-10-05 19:38:40 +02:00
hideCancel: boolean,
activeChannelClaim: ?ChannelClaim,
activeChannelStakedLevel: number,
2018-03-26 23:32:43 +02:00
};
const Header = (props: Props) => {
2019-08-21 22:54:44 +02:00
const {
balance,
2019-08-21 22:54:44 +02:00
roundedBalance,
roundedSpendableBalance,
2019-08-21 22:54:44 +02:00
history,
setClientSetting,
currentTheme,
automaticDarkModeEnabled,
hideBalance,
email,
authenticated,
2019-10-28 15:04:37 +01:00
authHeader,
2019-09-26 18:07:11 +02:00
signOut,
syncError,
openSignOutModal,
clearEmailEntry,
clearPasswordEntry,
emailToVerify,
2020-06-29 21:54:07 +02:00
backout,
2020-08-10 22:47:39 +02:00
sidebarOpen,
setSidebarOpen,
isAbsoluteSideNavHidden,
2020-09-29 22:15:35 +02:00
user,
2020-10-05 19:38:40 +02:00
hideCancel,
activeChannelClaim,
activeChannelStakedLevel,
2019-08-21 22:54:44 +02:00
} = props;
const isMobile = useIsMobile();
2019-11-22 22:13:00 +01:00
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
2019-11-14 01:33:36 +01:00
const isVerifyPage = history.location.pathname.includes(PAGES.AUTH_VERIFY);
const isSignUpPage = history.location.pathname.includes(PAGES.AUTH);
const isSignInPage = history.location.pathname.includes(PAGES.AUTH_SIGNIN);
const isPwdResetPage = history.location.pathname.includes(PAGES.AUTH_PASSWORD_RESET);
const hasBackout = Boolean(backout);
const { backLabel, backNavDefault, title: backTitle, simpleTitle: simpleBackTitle } = backout || {};
2020-12-11 19:33:27 +01:00
const notificationsEnabled = (user && user.experimental_ui) || false;
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
2020-01-20 17:47:03 +01:00
const authHeaderAction = syncError ? { onClick: signOut } : { navigate: '/' };
const homeButtonNavigationProps = isVerifyPage ? {} : authHeader ? authHeaderAction : { navigate: '/' };
const closeButtonNavigationProps = {
onClick: () => {
clearEmailEntry();
clearPasswordEntry();
if (syncError) {
signOut();
}
if (isSignInPage && !emailToVerify) {
history.goBack();
} else if (isSignUpPage) {
history.goBack();
} else if (isPwdResetPage) {
history.goBack();
} else {
history.push('/');
}
},
};
2018-03-26 23:32:43 +02:00
function onBackout(e) {
const { history, hasNavigated } = props;
const { goBack, replace } = history;
window.removeEventListener('popstate', onBackout);
if (e.type !== 'popstate') {
// if not initiated by pop (back button)
if (hasNavigated && !backNavDefault) {
goBack();
} else {
replace(backNavDefault || `/`);
}
}
}
2020-07-27 16:43:55 +02:00
React.useEffect(() => {
if (hasBackout) {
window.addEventListener('popstate', onBackout);
return () => window.removeEventListener('popstate', onBackout);
}
}, [hasBackout]);
2019-06-17 22:32:38 +02:00
function handleThemeToggle() {
if (automaticDarkModeEnabled) {
setClientSetting(SETTINGS.AUTOMATIC_DARK_MODE_ENABLED, false);
}
if (currentTheme === 'dark') {
2020-09-04 17:02:30 +02:00
setClientSetting(SETTINGS.THEME, 'light', true);
2019-06-17 22:32:38 +02:00
} else {
2020-09-04 17:02:30 +02:00
setClientSetting(SETTINGS.THEME, 'dark', true);
2019-06-17 22:32:38 +02:00
}
}
2018-03-26 23:32:43 +02:00
2020-11-10 06:21:04 +01:00
const loginButtons = (
<div className="header__auth-buttons">
2021-06-18 00:52:21 +02:00
<Button
navigate={`/$/${PAGES.AUTH_SIGNIN}`}
button="link"
label={__('Log In')}
className="mobile-hidden"
disabled={user === null}
/>
<Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Sign Up')} disabled={user === null} />
2020-11-10 06:21:04 +01:00
</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
/>
);
2017-06-06 23:19:12 +02:00
return (
<header
className={classnames('header', {
2019-10-28 15:04:37 +01:00
'header--minimal': authHeader,
// @if TARGET='app'
'header--mac': IS_MAC,
// @endif
})}
// @if TARGET='app'
onDoubleClick={(e) => {
remote.getCurrentWindow().maximize();
}}
// @endif
>
2019-06-11 20:10:58 +02:00
<div className="header__contents">
2020-06-29 21:54:07 +02:00
{!authHeader && backout ? (
<div className="card__actions--between">
<Button
onClick={onBackout}
button="link"
label={(backLabel && backLabel) || __('Cancel')}
icon={ICONS.ARROW_LEFT}
/>
2020-09-03 22:05:38 +02:00
{backTitle && <h1 className="header__auth-title">{isMobile ? simpleBackTitle || backTitle : backTitle}</h1>}
{authenticated || !IS_WEB ? (
<BalanceButton className="header__navigation-item menu__title" />
2020-11-10 06:21:04 +01:00
) : (
loginButtons
)}
2020-06-29 21:54:07 +02:00
</div>
) : (
<>
<div className="header__navigation">
2020-08-11 22:32:03 +02:00
{!authHeader && (
<span style={{ position: 'relative' }}>
<Button
aria-label={
sidebarOpen
? __('Close sidebar - hide channels you are following.')
: __('Expand sidebar - view channels you are following.')
}
2020-08-11 22:32:03 +02:00
className="header__navigation-item menu__title header__navigation-item--icon"
icon={ICONS.MENU}
aria-expanded={sidebarOpen}
2020-08-11 22:32:03 +02:00
onClick={() => setSidebarOpen(!sidebarOpen)}
>
2020-09-29 22:15:35 +02:00
{isAbsoluteSideNavHidden && isMobile && notificationsEnabled && <NotificationBubble />}
2020-08-11 22:32:03 +02:00
</Button>
</span>
)}
2020-06-29 21:54:07 +02:00
<Button
2020-12-11 19:33:27 +01:00
className="header__navigation-item header__navigation-item--lbry"
2020-07-09 00:31:23 +02:00
// @if TARGET='app'
label={'LBRY'}
// @endif
// @if TARGET='web'
label={LOGO_TITLE} // eslint-disable-line
// @endif
2020-06-29 21:54:07 +02:00
icon={ICONS.LBRY}
onClick={() => {
if (history.location.pathname === '/') window.location.reload();
}}
// @if TARGET='app'
onDoubleClick={(e) => {
2020-06-29 21:54:07 +02:00
e.stopPropagation();
}}
// @endif
{...homeButtonNavigationProps}
/>
{!authHeader && (
2020-08-10 22:47:39 +02:00
<div className="header__center">
{/* @if TARGET='app' */}
{!authHeader && (
<div className="header__buttons">
<NavigationButton isBackward history={history} />
<NavigationButton isBackward={false} history={history} />
</div>
)}
{/* @endif */}
2019-09-30 23:48:30 +02:00
2020-08-10 22:47:39 +02:00
{!authHeader && <WunderBar />}
2019-08-27 16:43:42 +02:00
2020-12-11 19:33:27 +01:00
<HeaderMenuButtons
authenticated={authenticated}
notificationsEnabled={notificationsEnabled}
history={history}
handleThemeToggle={handleThemeToggle}
currentTheme={currentTheme}
livestreamEnabled={livestreamEnabled}
2020-12-11 19:33:27 +01:00
/>
2020-08-10 22:47:39 +02:00
</div>
)}
</div>
2020-08-11 22:32:03 +02:00
{!authHeader && !backout ? (
2020-08-10 22:47:39 +02:00
<div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}>
{(!IS_WEB || authenticated) && (
<BalanceButton className="header__navigation-item menu__title mobile-hidden" />
2020-06-29 21:54:07 +02:00
)}
2020-07-23 16:22:57 +02:00
2020-11-10 06:21:04 +01:00
{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 improvements - [x] (6332) The IntersectionObserver method of lazy-loading loads cached images visibly late on slower devices. Previously, it was also showing the "broken image" icon briefly, which we mended by placing a dummy transparent image as the initial src. - Reverted that ugly transparent image fix. - Use the browser's built-in `loading="lazy"` instead. Sorry, Safari. - [x] Size-optimization did not take "device pixel ratio" into account. - When resizing an image through the CDN, we can't just take the dimensions of the tag in pixels directly -- we need to take zooming into account, otherwise the image ends up blurry. - Previously, we quickly disabled optimization for the channel avatar in the Channel Page because of this. Now that we know the root-cause, the change was reverted and we now go through the CDN with appropriate sizes. This also improves our Web Vital scores. - [x] Size-optimization wasn't really implemented for all ChannelThumbnail instances. - The CDN-optimized size was hardcoded to the largest instance, so small images like sidebar thumbnails are still loading images that are unnecessarily larger. - There's a little-bit of hardcoding of values from CSS here, but I think it's a ok compromise (not something we change often). It also doesn't need to be exact -- the "device pixel ratio" calculate will ensure it's slightly larger than what we need. - [x] Set `width` and `height` of `<img>` to improve CLS. - Addresses Ligthhouse complaints, although technically the shifting was addressed at the `ClaimPreviewTile` level (sub-container dimensions are well defined). - Notes: the values don't need to be the final CSS-adjusted sizes. It just needs to be in the right aspect ratio to help the browser pre-allocate space to avoid shifts. - [x] Add option to disable lazy-load Channel Thumbnails - The guidelines mentioned that items that are already in the viewport should not enable `loading="lazy"`. - We have a few areas where it doesn't make sense to lazy-load (e.g. thumbnail in Header, channel selector dropdown, publish preview, etc.).
2021-07-05 07:20:40 +02:00
<ChannelThumbnail uri={activeChannelUrl} small noLazyLoad />
) : (
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
)}
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOADS}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Uploads')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNELS}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('Channels')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CREATOR_DASHBOARD}`)}>
<Icon aria-hidden icon={ICONS.ANALYTICS} />
{__('Creator Analytics')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.REWARDS} />
{__('Rewards')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.INVITE}`)}>
<Icon aria-hidden icon={ICONS.INVITE} />
{__('Invites')}
</MenuItem>
{authenticated ? (
<MenuItem onSelect={IS_WEB ? signOut : openSignOutModal}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
</div>
<span className="menu__link-help">{email}</span>
</MenuItem>
) : !IS_WEB ? (
<>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
<Icon aria-hidden icon={ICONS.SIGN_UP} />
{__('Sign Up')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH_SIGNIN}`)}>
<Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')}
</MenuItem>
</>
) : null}
</MenuList>
</Menu>
)}
</div>
2020-06-29 21:54:07 +02:00
) : (
2020-10-05 19:38:40 +02:00
!isVerifyPage &&
!hideCancel && (
2020-06-29 21:54:07 +02:00
<div className="header__menu">
{/* Add an empty span here so we can use the same style as above */}
{/* This pushes the close button to the right side */}
<span />
<Button
title={__('Go Back')}
button="alt"
// className="button--header-close"
icon={ICONS.REMOVE}
{...closeButtonNavigationProps}
// @if TARGET='app'
onDoubleClick={(e) => {
e.stopPropagation();
}}
// @endif
/>
2020-06-29 21:54:07 +02:00
</div>
)
2019-08-27 16:43:42 +02:00
)}
2020-06-29 21:54:07 +02:00
</>
2019-08-27 16:43:42 +02:00
)}
2017-06-06 23:19:12 +02:00
</div>
</header>
);
2017-06-06 06:21:55 +02:00
};
2020-12-11 19:33:27 +01:00
type HeaderMenuButtonProps = {
authenticated: boolean,
notificationsEnabled: boolean,
history: { push: (string) => void },
handleThemeToggle: (string) => void,
2020-12-11 19:33:27 +01:00
currentTheme: string,
livestreamEnabled: boolean,
2020-12-11 19:33:27 +01:00
};
function HeaderMenuButtons(props: HeaderMenuButtonProps) {
2021-03-18 18:21:49 +01:00
const { authenticated, notificationsEnabled, history, handleThemeToggle, currentTheme, livestreamEnabled } = props;
2020-12-11 19:33:27 +01:00
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) => {
2020-12-11 19:33:27 +01:00
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.PUBLISH} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOAD}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Upload')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNEL_NEW}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('New Channel')}
</MenuItem>
2021-03-18 18:21:49 +01:00
{livestreamEnabled && (
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.LIVESTREAM}`)}>
<Icon aria-hidden icon={ICONS.VIDEO} />
{__('Go Live')}
</MenuItem>
)}
2020-12-11 19:33:27 +01:00
</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) => {
2020-12-11 19:33:27 +01:00
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.SETTINGS} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.SETTINGS}`)}>
<Icon aria-hidden tooltip icon={ICONS.SETTINGS} />
2020-12-11 19:33:27 +01:00
{__('Settings')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.HELP}`)}>
<Icon aria-hidden icon={ICONS.HELP} />
{__('Help')}
</MenuItem>
<MenuItem className="menu__link" onSelect={handleThemeToggle}>
<Icon icon={currentTheme === 'light' ? ICONS.DARK : ICONS.LIGHT} />
{currentTheme === 'light' ? __('Dark') : __('Light')}
</MenuItem>
</MenuList>
</Menu>
</div>
);
}
2019-06-17 22:32:38 +02:00
export default withRouter(Header);