collapsable sidebar initial commit

This commit is contained in:
Sean Yesmunt 2020-08-10 16:47:39 -04:00
parent f88b98ea62
commit 692862c769
45 changed files with 776 additions and 512 deletions

View file

@ -1275,5 +1275,6 @@
"URL": "URL", "URL": "URL",
"Total Uploads": "Total Uploads", "Total Uploads": "Total Uploads",
"Are you sure you want to purchase %claim_title%?": "Are you sure you want to purchase %claim_title%?", "Are you sure you want to purchase %claim_title%?": "Are you sure you want to purchase %claim_title%?",
"Discover": "Discover",
"--end--": "--end--" "--end--": "--end--"
} }

View file

@ -19,6 +19,7 @@ type Props = {
isBodyList?: boolean, isBodyList?: boolean,
defaultExpand?: boolean, defaultExpand?: boolean,
nag?: Node, nag?: Node,
smallTitle?: boolean,
}; };
export default function Card(props: Props) { export default function Card(props: Props) {
@ -33,6 +34,7 @@ export default function Card(props: Props) {
isPageTitle = false, isPageTitle = false,
isBodyList = false, isBodyList = false,
noTitleWrap = false, noTitleWrap = false,
smallTitle = false,
defaultExpand, defaultExpand,
nag, nag,
} = props; } = props;
@ -47,11 +49,18 @@ export default function Card(props: Props) {
'card__header--nowrap': noTitleWrap, 'card__header--nowrap': noTitleWrap,
})} })}
> >
<div className={classnames('card__title-section', { 'card__title-section--body-list': isBodyList })}> <div
className={classnames('card__title-section', {
'card__title-section--body-list': isBodyList,
'card__title-section--small': smallTitle,
})}
>
{icon && <Icon sectionIcon icon={icon} />} {icon && <Icon sectionIcon icon={icon} />}
<div> <div>
{isPageTitle && <h1 className="card__title">{title}</h1>} {isPageTitle && <h1 className="card__title">{title}</h1>}
{!isPageTitle && <h2 className="card__title">{title}</h2>} {!isPageTitle && (
<h2 className={classnames('card__title', { 'card__title--small': smallTitle })}>{title}</h2>
)}
{subtitle && <div className="card__subtitle">{subtitle}</div>} {subtitle && <div className="card__subtitle">{subtitle}</div>}
</div> </div>
</div> </div>

View file

@ -3,7 +3,7 @@ import React from 'react';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import ReactPaginate from 'react-paginate'; import ReactPaginate from 'react-paginate';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
const PAGINATE_PARAM = 'page'; const PAGINATE_PARAM = 'page';

View file

@ -3,7 +3,7 @@ import React, { useEffect } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle'; import FileViewerEmbeddedTitle from 'component/fileViewerEmbeddedTitle';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb } from 'util/url';
type Props = { type Props = {

View file

@ -10,7 +10,7 @@ import Button from 'component/button';
import FileDownloadLink from 'component/fileDownloadLink'; import FileDownloadLink from 'component/fileDownloadLink';
import { buildURI } from 'lbry-redux'; import { buildURI } from 'lbry-redux';
import * as RENDER_MODES from 'constants/file_render_modes'; import * as RENDER_MODES from 'constants/file_render_modes';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
import ClaimSupportButton from 'component/claimSupportButton'; import ClaimSupportButton from 'component/claimSupportButton';
type Props = { type Props = {

View file

@ -1,5 +1,5 @@
// @flow // @flow
import React, { Fragment, PureComponent } from 'react'; import React, { PureComponent } from 'react';
import MarkdownPreview from 'component/common/markdown-preview'; import MarkdownPreview from 'component/common/markdown-preview';
import ClaimTags from 'component/claimTags'; import ClaimTags from 'component/claimTags';
import Card from 'component/common/card'; import Card from 'component/common/card';
@ -23,23 +23,22 @@ class FileDescription extends PureComponent<Props> {
const { description } = metadata; const { description } = metadata;
if (!description && !(tags && tags.length)) return null; if (!description && !(tags && tags.length)) return null;
return ( return (
<Fragment> <Card
<Card title={__('Description')}
title={__('Description')} defaultExpand
defaultExpand actions={
actions={ <>
<> {description && (
{description && ( <div className="media__info-text">
<div className="media__info-text"> <MarkdownPreview content={description} />
<MarkdownPreview content={description} /> </div>
</div> )}
)} <ClaimTags uri={uri} type="large" />
<ClaimTags uri={uri} type="large" /> </>
</> }
} />
/>
</Fragment>
); );
} }
} }

View file

@ -12,7 +12,7 @@ import { FILE_WRAPPER_CLASS } from 'page/file/view';
import Draggable from 'react-draggable'; import Draggable from 'react-draggable';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
import { onFullscreenChange } from 'util/full-screen'; import { onFullscreenChange } from 'util/full-screen';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
type Props = { type Props = {
isFloating: boolean, isFloating: boolean,

View file

@ -28,7 +28,6 @@ const perform = dispatch => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
syncSettings: () => dispatch(doSyncClientSettings()), syncSettings: () => dispatch(doSyncClientSettings()),
signOut: () => dispatch(doSignOut()), signOut: () => dispatch(doSignOut()),
openMobileNavigation: () => dispatch(doOpenModal(MODALS.MOBILE_NAVIGATION)),
openChannelCreate: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)), openChannelCreate: () => dispatch(doOpenModal(MODALS.CREATE_CHANNEL)),
openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)), openSignOutModal: () => dispatch(doOpenModal(MODALS.SIGN_OUT)),
clearEmailEntry: () => dispatch(doClearEmailEntry()), clearEmailEntry: () => dispatch(doClearEmailEntry()),

View file

@ -12,9 +12,11 @@ import Icon from 'component/common/icon';
import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button'; import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
import NavigationButton from 'component/navigationButton'; import NavigationButton from 'component/navigationButton';
import NotificationHeaderButton from 'component/notificationHeaderButton';
import { LOGO_TITLE } from 'config'; import { LOGO_TITLE } from 'config';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
import NotificationBubble from 'component/notificationBubble';
import NotificationHeaderButton from 'component/notificationHeaderButton';
// @if TARGET='app' // @if TARGET='app'
import { remote } from 'electron'; import { remote } from 'electron';
import { IS_MAC } from 'component/app/view'; import { IS_MAC } from 'component/app/view';
@ -49,13 +51,15 @@ type Props = {
syncError: ?string, syncError: ?string,
emailToVerify?: string, emailToVerify?: string,
signOut: () => void, signOut: () => void,
openMobileNavigation: () => void,
openChannelCreate: () => void, openChannelCreate: () => void,
openSignOutModal: () => void, openSignOutModal: () => void,
clearEmailEntry: () => void, clearEmailEntry: () => void,
clearPasswordEntry: () => void, clearPasswordEntry: () => void,
hasNavigated: boolean, hasNavigated: boolean,
syncSettings: () => void, syncSettings: () => void,
sidebarOpen: boolean,
setSidebarOpen: boolean => void,
isAbsoluteSideNavHidden: boolean,
}; };
const Header = (props: Props) => { const Header = (props: Props) => {
@ -71,13 +75,15 @@ const Header = (props: Props) => {
authHeader, authHeader,
signOut, signOut,
syncError, syncError,
openMobileNavigation,
openSignOutModal, openSignOutModal,
clearEmailEntry, clearEmailEntry,
clearPasswordEntry, clearPasswordEntry,
emailToVerify, emailToVerify,
backout, backout,
syncSettings, syncSettings,
sidebarOpen,
setSidebarOpen,
isAbsoluteSideNavHidden,
} = props; } = props;
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
@ -164,9 +170,6 @@ const Header = (props: Props) => {
<header <header
className={classnames('header', { className={classnames('header', {
'header--minimal': authHeader, 'header--minimal': authHeader,
// @if TARGET='web'
'header--noauth-web': !authenticated,
// @endif
// @if TARGET='app' // @if TARGET='app'
'header--mac': IS_MAC, 'header--mac': IS_MAC,
// @endif // @endif
@ -203,6 +206,14 @@ const Header = (props: Props) => {
) : ( ) : (
<> <>
<div className="header__navigation"> <div className="header__navigation">
<span style={{ position: 'relative' }}>
<Button
className="header__navigation-item menu__title header__navigation-item--icon"
icon={ICONS.MENU}
onClick={() => setSidebarOpen(!sidebarOpen)}
/>
{isAbsoluteSideNavHidden && <NotificationBubble />}
</span>
<Button <Button
className="header__navigation-item header__navigation-item--lbry header__navigation-item--button-mobile" className="header__navigation-item header__navigation-item--lbry header__navigation-item--button-mobile"
// @if TARGET='app' // @if TARGET='app'
@ -223,33 +234,20 @@ const Header = (props: Props) => {
{...homeButtonNavigationProps} {...homeButtonNavigationProps}
/> />
{/* @if TARGET='app' */}
{!authHeader && ( {!authHeader && (
<div className="header__navigation-arrows"> <div className="header__center">
<NavigationButton isBackward history={history} /> {/* @if TARGET='app' */}
<NavigationButton isBackward={false} history={history} /> {!authHeader && (
</div> <div className="header__buttons">
)} <NavigationButton isBackward history={history} />
{/* @endif */} <NavigationButton isBackward={false} history={history} />
</div>
)}
{/* @endif */}
{!authHeader && <WunderBar />} {!authHeader && <WunderBar />}
</div>
{!authHeader ? ( <div className="header__buttons mobile-hidden">
<div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}>
{(!IS_WEB || authenticated) && (
<Fragment>
<Button
aria-label={__('Your wallet')}
navigate={`/$/${PAGES.WALLET}`}
className="header__navigation-item menu__title header__navigation-item--balance"
label={getWalletTitle()}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
<Menu> <Menu>
<MenuButton <MenuButton
aria-label={__('Publish a file, or create a channel')} aria-label={__('Publish a file, or create a channel')}
@ -263,6 +261,7 @@ const Header = (props: Props) => {
> >
<Icon size={18} icon={ICONS.PUBLISH} aria-hidden /> <Icon size={18} icon={ICONS.PUBLISH} aria-hidden />
</MenuButton> </MenuButton>
<NotificationHeaderButton />
<MenuList className="menu__list--header"> <MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOAD}`)}> <MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.UPLOAD}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} /> <Icon aria-hidden icon={ICONS.PUBLISH} />
@ -275,8 +274,6 @@ const Header = (props: Props) => {
</MenuList> </MenuList>
</Menu> </Menu>
<NotificationHeaderButton />
<Menu> <Menu>
<MenuButton <MenuButton
aria-label={__('Your account')} aria-label={__('Your account')}
@ -334,37 +331,57 @@ const Header = (props: Props) => {
)} )}
</MenuList> </MenuList>
</Menu> </Menu>
<Menu>
<MenuButton
aria-label={__('Settings')}
title={__('Settings')}
className="header__navigation-item menu__title header__navigation-item--icon"
// @if TARGET='app'
onDoubleClick={e => {
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 tootlip icon={ICONS.SETTINGS} />
{__('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>
</div>
)}
</div>
{!authHeader ? (
<div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}>
{(!IS_WEB || authenticated) && (
<Fragment>
<Button
aria-label={__('Your wallet')}
navigate={`/$/${PAGES.WALLET}`}
className="header__navigation-item menu__title header__navigation-item--balance"
label={getWalletTitle()}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
</Fragment> </Fragment>
)} )}
<Menu>
<MenuButton
aria-label={__('Settings')}
title={__('Settings')}
className="header__navigation-item menu__title header__navigation-item--icon"
// @if TARGET='app'
onDoubleClick={e => {
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 tootlip icon={ICONS.SETTINGS} />
{__('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>
{IS_WEB && !authenticated && ( {IS_WEB && !authenticated && (
<div className="header__auth-buttons"> <div className="header__auth-buttons">
<Button navigate={`/$/${PAGES.AUTH_SIGNIN}`} button="link" label={__('Sign In')} /> <Button navigate={`/$/${PAGES.AUTH_SIGNIN}`} button="link" label={__('Sign In')} />
@ -394,17 +411,6 @@ const Header = (props: Props) => {
</div> </div>
) )
)} )}
<Button
onClick={openMobileNavigation}
icon={ICONS.MENU}
iconSize={24}
className="header__menu--mobile"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
</> </>
)} )}
</div> </div>

View file

@ -0,0 +1,9 @@
import { connect } from 'react-redux';
import { selectUnreadNotificationCount } from 'redux/selectors/notifications';
import NotificationHeaderButton from './view';
const select = state => ({
unreadCount: selectUnreadNotificationCount(state),
});
export default connect(select)(NotificationHeaderButton);

View file

@ -0,0 +1,20 @@
// @flow
import React from 'react';
type Props = {
unreadCount: number,
};
export default function NotificationHeaderButton(props: Props) {
const { unreadCount } = props;
if (unreadCount === 0) {
return null;
}
return (
<span className="notification__bubble">
<span className="notification__count">{unreadCount}</span>
</span>
);
}

View file

@ -8,6 +8,10 @@ import Footer from 'web/component/footer';
/* @if TARGET='app' */ /* @if TARGET='app' */
import StatusBar from 'component/common/status-bar'; import StatusBar from 'component/common/status-bar';
/* @endif */ /* @endif */
import usePersistedState from 'effects/use-persisted-state';
import { useHistory } from 'react-router';
import { useIsMobile, useIsMediumScreen } from 'effects/use-screensize';
import { parseURI } from 'lbry-redux';
export const MAIN_CLASS = 'main'; export const MAIN_CLASS = 'main';
type Props = { type Props = {
@ -16,9 +20,11 @@ type Props = {
autoUpdateDownloaded: boolean, autoUpdateDownloaded: boolean,
isUpgradeAvailable: boolean, isUpgradeAvailable: boolean,
authPage: boolean, authPage: boolean,
filePage: boolean,
noHeader: boolean, noHeader: boolean,
noFooter: boolean, noFooter: boolean,
noSideNavigation: boolean, noSideNavigation: boolean,
fullWidth: boolean,
backout: { backout: {
backLabel?: string, backLabel?: string,
backNavDefault?: string, backNavDefault?: string,
@ -32,18 +38,61 @@ function Page(props: Props) {
children, children,
className, className,
authPage = false, authPage = false,
filePage = false,
noHeader = false, noHeader = false,
noFooter = false, noFooter = false,
noSideNavigation = false, noSideNavigation = false,
backout, backout,
} = props; } = props;
const {
location: { pathname },
} = useHistory();
const [sidebarOpen, setSidebarOpen] = usePersistedState('sidebar', true);
const isMediumScreen = useIsMediumScreen();
const isMobile = useIsMobile();
let isOnFilePage = false;
try {
const url = pathname.slice(1).replace(/:/g, '#');
const { isChannel } = parseURI(url);
if (!isChannel) {
isOnFilePage = true;
}
} catch (e) {}
const isAbsoluteSideNavHidden = (isOnFilePage || isMobile) && !sidebarOpen;
React.useEffect(() => {
if (isOnFilePage || isMediumScreen) {
setSidebarOpen(false);
}
}, [isOnFilePage, isMediumScreen]);
return ( return (
<Fragment> <Fragment>
{!noHeader && <Header authHeader={authPage} backout={backout} />} {!noHeader && (
<div className={classnames('main-wrapper__inner')}> <Header
<main className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage })}>{children}</main> authHeader={authPage}
{!authPage && !noSideNavigation && <SideNavigation />} backout={backout}
sidebarOpen={sidebarOpen}
isAbsoluteSideNavHidden={isAbsoluteSideNavHidden}
setSidebarOpen={setSidebarOpen}
/>
)}
<div className={classnames('main-wrapper__inner', { 'main-wrapper__inner--filepage': isOnFilePage })}>
{!authPage && !noSideNavigation && (
<SideNavigation
sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen}
isMediumScreen={isMediumScreen}
isOnFilePage={isOnFilePage}
/>
)}
<main
className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage, 'main--file-page': filePage })}
>
{children}
</main>
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
<StatusBar /> <StatusBar />
{/* @endif */} {/* @endif */}

View file

@ -71,6 +71,8 @@ export default class RecommendedContent extends React.PureComponent<Props> {
return ( return (
<Card <Card
isBodyList isBodyList
smallTitle
className="file-page__recommended"
title={__('Related')} title={__('Related')}
body={ body={
<WaitUntilOnPage lastUpdateDate={this.lastReset}> <WaitUntilOnPage lastUpdateDate={this.lastReset}>

View file

@ -212,13 +212,7 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.AUTH}`} exact component={SignUpPage} /> <Route path={`/$/${PAGES.AUTH}`} exact component={SignUpPage} />
<Route path={`/$/${PAGES.AUTH}/*`} exact component={SignUpPage} /> <Route path={`/$/${PAGES.AUTH}/*`} exact component={SignUpPage} />
<Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} /> <Route path={`/$/${PAGES.WELCOME}`} exact component={Welcome} />
<Route path={`/$/${PAGES.TAGS_FOLLOWING}`} exact component={TagsFollowingPage} />
<Route
path={`/$/${PAGES.CHANNELS_FOLLOWING}`}
exact
component={isAuthenticated || !IS_WEB ? ChannelsFollowingPage : DiscoverPage}
/>
<Route path={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`} exact component={ChannelsFollowingDiscoverPage} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} /> <Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
<Route path={`/$/${PAGES.BACKUP}`} exact component={BackupPage} /> <Route path={`/$/${PAGES.BACKUP}`} exact component={BackupPage} />
@ -235,11 +229,17 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} /> <Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} />
<Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} /> <Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TAGS_FOLLOWING}`} component={TagsFollowingPage} />
<PrivateRoute <PrivateRoute
{...props} {...props}
path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} path={`/$/${PAGES.CHANNELS_FOLLOWING}`}
exact component={isAuthenticated || !IS_WEB ? ChannelsFollowingPage : DiscoverPage}
component={SettingsNotificationsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} component={SettingsNotificationsPage} />
<PrivateRoute
{...props}
path={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
component={ChannelsFollowingDiscoverPage}
/> />
<PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} /> <PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNEL_NEW}`} component={ChannelNew} /> <PrivateRoute {...props} path={`/$/${PAGES.CHANNEL_NEW}`} component={ChannelNew} />

View file

@ -1,19 +1,19 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectFollowedTags, selectPurchaseUriSuccess, doClearPurchasedUriSuccess, SETTINGS } from 'lbry-redux'; import { selectPurchaseUriSuccess, doClearPurchasedUriSuccess, SETTINGS } from 'lbry-redux';
import { selectUploadCount } from 'lbryinc';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSignOut } from 'redux/actions/app'; import { doSignOut } from 'redux/actions/app';
import { selectUnreadNotificationCount } from 'redux/selectors/notifications';
import SideNavigation from './view'; import SideNavigation from './view';
const select = state => ({ const select = state => ({
subscriptions: selectSubscriptions(state), subscriptions: selectSubscriptions(state),
followedTags: selectFollowedTags(state),
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
uploadCount: selectUploadCount(state),
email: selectUserVerifiedEmail(state), email: selectUserVerifiedEmail(state),
purchaseSuccess: selectPurchaseUriSuccess(state), purchaseSuccess: selectPurchaseUriSuccess(state),
unreadCount: selectUnreadNotificationCount(state),
}); });
export default connect(select, { export default connect(select, {

View file

@ -1,31 +1,129 @@
// @flow // @flow
import type { Node } from 'react';
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import { withRouter } from 'react-router';
import Button from 'component/button'; import Button from 'component/button';
import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext'; import StickyBox from 'react-sticky-box/dist/esnext';
import Spinner from 'component/spinner';
import usePersistedState from 'effects/use-persisted-state';
import classnames from 'classnames'; import classnames from 'classnames';
import { PINNED_LABEL_1, PINNED_URI_1, PINNED_URI_2, PINNED_LABEL_2 } from 'config'; import NotificationBubble from 'component/notificationBubble';
// @if TARGET='web'
// import Ads from 'web/component/ads';
// @endif
const SHOW_CHANNELS = 'SHOW_CHANNELS'; const ESCAPE_KEY_CODE = 27;
const SHOW_TAGS = 'SHOW_TAGS'; const BACKSLASH_KEY_CODE = 220;
const TOP_LEVEL_LINKS: Array<{
label: string,
navigate: string,
icon: string,
extra?: Node,
hideForUnauth?: boolean,
}> = [
{
label: __('Home'),
navigate: `/`,
icon: ICONS.HOME,
},
{
label: __('Following'),
navigate: `/$/${PAGES.CHANNELS_FOLLOWING}`,
icon: ICONS.SUBSCRIBE,
},
{
label: __('Your Tags'),
navigate: `/$/${PAGES.TAGS_FOLLOWING}`,
icon: ICONS.TAG,
hideForUnauth: true,
},
{
label: __('Discover'),
navigate: `/$/${PAGES.DISCOVER}`,
icon: ICONS.DISCOVER,
},
{
label: __('Purchased'),
navigate: `/$/${PAGES.LIBRARY}`,
icon: ICONS.PURCHASED,
hideForUnauth: true,
},
];
const ABSOLUTE_LINKS: Array<{
label: string,
navigate: string,
icon: string,
extra?: Node,
hideForUnauth?: boolean,
}> = [
{
label: __('Upload'),
navigate: `/$/${PAGES.UPLOAD}`,
icon: ICONS.PUBLISH,
},
{
label: __('New Channel'),
navigate: `/$/${PAGES.CHANNEL_NEW}`,
icon: ICONS.CHANNEL,
},
{
label: __('Uploads'),
navigate: `/$/${PAGES.UPLOADS}`,
icon: ICONS.PUBLISH,
hideForUnauth: true,
},
{
label: __('Channels'),
navigate: `/$/${PAGES.CHANNELS}`,
icon: ICONS.CHANNEL,
hideForUnauth: true,
},
{
label: __('Creator Analytics'),
navigate: `/$/${PAGES.CREATOR_ANALYTICS}`,
icon: ICONS.ANALYTICS,
},
{
label: __('Notifications'),
navigate: `/$/${PAGES.NOTIFICATIONS}`,
icon: ICONS.NOTIFICATION,
extra: <NotificationBubble />,
hideForUnauth: true,
},
{
label: __('Rewards'),
navigate: `/$/${PAGES.REWARDS}`,
icon: ICONS.REWARDS,
hideForUnauth: true,
},
{
label: __('Invites'),
navigate: `/$/${PAGES.INVITES}`,
icon: ICONS.INVITE,
hideForUnauth: true,
},
{
label: __('Settings'),
navigate: `/$/${PAGES.SETTINGS}`,
icon: ICONS.SETTINGS,
hideForUnauth: true,
},
{
label: __('Help'),
navigate: `/$/${PAGES.HELP}`,
icon: ICONS.HELP,
hideForUnauth: true,
},
];
type Props = { type Props = {
subscriptions: Array<Subscription>, subscriptions: Array<Subscription>,
followedTags: Array<Tag>,
email: ?string, email: ?string,
uploadCount: number, uploadCount: number,
sticky: boolean,
expanded: boolean,
doSignOut: () => void, doSignOut: () => void,
location: { pathname: string }, sidebarOpen: boolean,
setSidebarOpen: boolean => void,
isMediumScreen: boolean,
isOnFilePage: boolean,
unreadCount: number,
purchaseSuccess: boolean, purchaseSuccess: boolean,
doClearPurchasedUriSuccess: () => void, doClearPurchasedUriSuccess: () => void,
}; };
@ -33,44 +131,20 @@ type Props = {
function SideNavigation(props: Props) { function SideNavigation(props: Props) {
const { const {
subscriptions, subscriptions,
followedTags, // doSignOut,
uploadCount,
doSignOut,
email, email,
sticky = true,
expanded = false,
location,
purchaseSuccess, purchaseSuccess,
doClearPurchasedUriSuccess, doClearPurchasedUriSuccess,
sidebarOpen,
setSidebarOpen,
isMediumScreen,
isOnFilePage,
unreadCount,
} = props; } = props;
const { pathname } = location;
const isAuthenticated = Boolean(email); const isAuthenticated = Boolean(email);
const [pulseLibrary, setPulseLibrary] = React.useState(false); const [pulseLibrary, setPulseLibrary] = React.useState(false);
const [sideInformation, setSideInformation] = usePersistedState(
'side-navigation:information',
getSideInformation(pathname)
);
const isPersonalized = !IS_WEB || isAuthenticated; const isPersonalized = !IS_WEB || isAuthenticated;
const requireAuthOnPersonalizedActions = IS_WEB; const isAbsolute = isOnFilePage || isMediumScreen;
function getSideInformation(path) {
switch (path) {
case `/$/${PAGES.CHANNELS_FOLLOWING}`:
return SHOW_CHANNELS;
case `/$/${PAGES.TAGS_FOLLOWING}`:
return SHOW_TAGS;
default:
return sideInformation;
}
}
React.useEffect(() => {
const sideInfo = getSideInformation(pathname);
setSideInformation(sideInfo);
}, [pathname, setSideInformation]);
React.useEffect(() => { React.useEffect(() => {
if (purchaseSuccess) { if (purchaseSuccess) {
@ -85,172 +159,119 @@ function SideNavigation(props: Props) {
} }
}, [setPulseLibrary, purchaseSuccess, doClearPurchasedUriSuccess]); }, [setPulseLibrary, purchaseSuccess, doClearPurchasedUriSuccess]);
function buildLink(path, label, icon, onClick, requiresAuth = false, isLiteral = false) { React.useEffect(() => {
return { function handleKeydown(e: SyntheticKeyboardEvent<*>) {
navigate: !isLiteral ? `$/${path}` : `${path}`, if (e.keyCode === ESCAPE_KEY_CODE && isAbsolute) {
label, setSidebarOpen(false);
icon, } else if (e.keyCode === BACKSLASH_KEY_CODE) {
onClick, const hasActiveInput = document.querySelector('input:focus');
requiresAuth, if (!hasActiveInput) {
}; setSidebarOpen(!sidebarOpen);
} }
}
}
window.addEventListener('keydown', handleKeydown);
return () => window.removeEventListener('keydown', handleKeydown);
}, [sidebarOpen, setSidebarOpen, isAbsolute]);
const Wrapper = ({ children }: any) => const Wrapper = ({ children }: any) =>
sticky ? ( !isOnFilePage && !isMediumScreen ? (
<StickyBox offsetTop={100} offsetBottom={20}> <StickyBox offsetTop={100} offsetBottom={20}>
{children} {children}
</StickyBox> </StickyBox>
) : ( ) : (
<div>{children}</div> children
); );
return ( return (
<Wrapper> <Wrapper>
<nav className="navigation"> {!isOnFilePage && (
<ul className="navigation-links"> <nav className={classnames('navigation', { 'navigation--micro': !sidebarOpen || isMediumScreen })}>
{[ <ul className={classnames('navigation-links--relative', { 'navigation-links--micro': !sidebarOpen })}>
{ {TOP_LEVEL_LINKS.map(linkProps =>
...(expanded && !isAuthenticated ? { ...buildLink(PAGES.AUTH, __('Sign Up'), ICONS.SIGN_UP) } : {}), !email && linkProps.hideForUnauth && IS_WEB ? null : (
},
{
...(expanded && !isAuthenticated
? { ...buildLink(PAGES.AUTH_SIGNIN, __('Sign In'), ICONS.SIGN_IN) }
: {}),
},
{
...buildLink('/', __('Home'), ICONS.HOME, null, null, true),
},
{
...buildLink(
PAGES.CHANNELS_FOLLOWING,
__('Following'),
ICONS.SUBSCRIBE,
null,
requireAuthOnPersonalizedActions
),
},
{
...buildLink(PAGES.TAGS_FOLLOWING, __('Your Tags'), ICONS.TAG, null, requireAuthOnPersonalizedActions),
},
{
...buildLink(PAGES.DISCOVER, __('All Content'), ICONS.DISCOVER),
},
{
...buildLink(PAGES.LIBRARY, IS_WEB ? __('Purchased') : __('Library'), ICONS.LIBRARY),
},
// @if TARGET='web'
{
...(PINNED_URI_1
? { ...buildLink(`${PINNED_URI_1}`, `${PINNED_LABEL_1}`, ICONS.PINNED, null, null, true) }
: {}),
},
{
...(PINNED_URI_2
? { ...buildLink(`${PINNED_URI_2}`, `${PINNED_LABEL_2}`, ICONS.PINNED, null, null, true) }
: {}),
},
// @endif
{
...(expanded ? { ...buildLink(PAGES.SETTINGS, __('Settings'), ICONS.SETTINGS) } : {}),
},
].map(
linkProps =>
linkProps.navigate && (
<li key={linkProps.navigate}> <li key={linkProps.navigate}>
<Button <Button
{...linkProps} {...linkProps}
icon={pulseLibrary && linkProps.icon === ICONS.LIBRARY ? ICONS.PURCHASED : linkProps.icon} icon={pulseLibrary && linkProps.icon === ICONS.LIBRARY ? ICONS.PURCHASED : linkProps.icon}
className={classnames('navigation-link', { className={classnames('navigation-link', {
'navigation-link--pulse': linkProps.icon === ICONS.LIBRARY && pulseLibrary, 'navigation-link--pulse': linkProps.icon === ICONS.LIBRARY && pulseLibrary,
'navigation-link--extra': linkProps.icon === ICONS.NOTIFICATION && unreadCount > 0,
})} })}
activeClass="navigation-link--active" activeClass="navigation-link--active"
/> />
{linkProps.extra}
</li> </li>
) )
)}
{expanded &&
isPersonalized &&
[
{
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
},
{
...buildLink(
PAGES.UPLOADS,
uploadCount ? (
<span>
{__('Uploads')}
<Spinner type="small" />
</span>
) : (
__('Uploads')
),
ICONS.PUBLISH
),
},
{
...buildLink(PAGES.CREATOR_DASHBOARD, __('Creator Analytics'), ICONS.ANALYTICS),
},
{
...buildLink(PAGES.WALLET, __('Wallet'), ICONS.WALLET),
},
{
...buildLink(PAGES.REWARDS, __('Rewards'), ICONS.REWARDS),
},
{
...buildLink(PAGES.INVITE, __('Invites'), ICONS.INVITE),
},
{
...buildLink(PAGES.UPLOAD, __('Upload'), ICONS.PUBLISH),
},
{
...buildLink(PAGES.HELP, __('Help'), ICONS.HELP),
},
{
...(isAuthenticated ? { ...buildLink(PAGES.AUTH, __('Sign Out'), ICONS.SIGN_OUT, doSignOut) } : {}),
},
].map(
linkProps =>
Object.keys(linkProps).length > 0 &&
linkProps && (
<li key={linkProps.navigate}>
<Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" />
</li>
)
)} )}
</ul>
{sideInformation === SHOW_TAGS && !expanded && isPersonalized && (
<ul className="navigation__secondary navigation-links--small tags--vertical">
{followedTags.map(({ name }, key) => (
<li className="navigation-link__wrapper" key={name}>
<Tag navigate={`/$/tags?t${name}`} name={name} />
</li>
))}
</ul> </ul>
)}
{sideInformation === SHOW_CHANNELS && !expanded && isPersonalized && ( {sidebarOpen && isPersonalized && subscriptions && subscriptions.length > 0 && (
<ul className="navigation__secondary navigation-links--small"> <ul className="navigation__secondary navigation-links--relative navigation-links--small">
{subscriptions.map(({ uri, channelName }, index) => ( {subscriptions.map(({ uri, channelName }, index) => (
<li key={uri} className="navigation-link__wrapper"> <li key={uri} className="navigation-link__wrapper">
<Button <Button
navigate={uri} navigate={uri}
label={channelName} label={channelName}
className="navigation-link" className="navigation-link"
activeClass="navigation-link--active" activeClass="navigation-link--active"
/> />
</li> </li>
))} ))}
</ul> </ul>
)} )}
</nav> </nav>
// @if TARGET='web' )}
{/* {!isAuthenticated && !expanded && <Ads />} commenting out sidebar ads for test */}
// @endif {(isOnFilePage || isMediumScreen) && sidebarOpen && (
<>
<nav className={classnames('navigation--filepage')}>
<ul className="navigation-links--absolute">
{TOP_LEVEL_LINKS.map(linkProps => (
<li key={linkProps.navigate}>
<Button
{...linkProps}
icon={pulseLibrary && linkProps.icon === ICONS.LIBRARY ? ICONS.PURCHASED : linkProps.icon}
className={classnames('navigation-link', {
'navigation-link--pulse': linkProps.icon === ICONS.LIBRARY && pulseLibrary,
'navigation-link--extra': linkProps.icon === ICONS.NOTIFICATION && unreadCount > 0,
})}
activeClass="navigation-link--active"
/>
{linkProps.extra}
</li>
))}
</ul>
<ul className="navigation-links--absolute">
{ABSOLUTE_LINKS.map(linkProps => (
<li key={linkProps.navigate}>
<Button {...linkProps} className="navigation-link" activeClass="navigation-link--active" />
{linkProps.extra}
</li>
))}
</ul>
{isPersonalized && subscriptions && subscriptions.length > 0 && (
<ul className="navigation__secondary navigation-links--small">
{subscriptions.map(({ uri, channelName }, index) => (
<li key={uri} className="navigation-link__wrapper">
<Button
navigate={uri}
label={channelName}
className="navigation-link"
activeClass="navigation-link--active"
/>
</li>
))}
</ul>
)}
</nav>
<div className="navigation__overlay" onClick={() => setSidebarOpen(false)} />
</>
)}
</Wrapper> </Wrapper>
); );
} }
export default withRouter(SideNavigation); export default SideNavigation;

View file

@ -5,7 +5,7 @@ import Button from 'component/button';
import CopyableText from 'component/copyableText'; import CopyableText from 'component/copyableText';
import EmbedTextArea from 'component/embedTextArea'; import EmbedTextArea from 'component/embedTextArea';
import { generateDownloadUrl } from 'util/web'; import { generateDownloadUrl } from 'util/web';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import { hmsToSeconds, secondsToHms } from 'util/time'; import { hmsToSeconds, secondsToHms } from 'util/time';
import { import {

View file

@ -5,7 +5,7 @@ import React, { useRef } from 'react';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import Button from 'component/button'; import Button from 'component/button';
import useHover from 'effects/use-hover'; import useHover from 'effects/use-hover';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
type SubscriptionArgs = { type SubscriptionArgs = {
channelName: string, channelName: string,

View file

@ -34,7 +34,6 @@ export const WALLET_SEND = 'wallet_send';
export const WALLET_RECEIVE = 'wallet_receive'; export const WALLET_RECEIVE = 'wallet_receive';
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 MOBILE_NAVIGATION = 'mobile_navigation';
export const SET_REFERRER = 'set_referrer'; export const SET_REFERRER = 'set_referrer';
export const REPOST = 'repost'; export const REPOST = 'repost';
export const SIGN_OUT = 'sign_out'; export const SIGN_OUT = 'sign_out';

View file

@ -1,6 +0,0 @@
import useMedia from './use-media';
export default function useIsMobile() {
const isMobile = useMedia(['(min-width: 901px)'], [false], true);
return isMobile;
}

View file

@ -0,0 +1,11 @@
import useMedia from './use-media';
export function useIsMobile() {
const isMobile = useMedia(['(min-width: 901px)'], [false], true);
return isMobile;
}
export function useIsMediumScreen() {
const isMobile = useMedia(['(min-width: 1151px)'], [false], true);
return isMobile;
}

View file

@ -5,7 +5,7 @@ import * as React from 'react';
import ReactModal from 'react-modal'; import ReactModal from 'react-modal';
import Button from 'component/button'; import Button from 'component/button';
import classnames from 'classnames'; import classnames from 'classnames';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
type ModalProps = { type ModalProps = {
type?: string, type?: string,

View file

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

View file

@ -1,18 +0,0 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import SideNavigation from 'component/sideNavigation';
type Props = {
doHideModal: () => void,
};
export default function ModalMobileNavigation(props: Props) {
const { doHideModal } = props;
return (
<Modal type="card" isOpen contentLabel={__('Navigation')} onAborted={doHideModal}>
<SideNavigation sticky={false} expanded />
</Modal>
);
}

View file

@ -32,7 +32,6 @@ import ModalWalletSend from 'modal/modalWalletSend';
import ModalWalletReceive from 'modal/modalWalletReceive'; import ModalWalletReceive from 'modal/modalWalletReceive';
import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome'; import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome';
import ModalCreateChannel from 'modal/modalChannelCreate'; import ModalCreateChannel from 'modal/modalChannelCreate';
import ModalMobileNavigation from 'modal/modalMobileNavigation';
import ModalSetReferrer from 'modal/modalSetReferrer'; import ModalSetReferrer from 'modal/modalSetReferrer';
import ModalRepost from 'modal/modalRepost'; import ModalRepost from 'modal/modalRepost';
import ModalSignOut from 'modal/modalSignOut'; import ModalSignOut from 'modal/modalSignOut';
@ -125,8 +124,6 @@ function ModalRouter(props: Props) {
return <ModalYoutubeWelcome />; return <ModalYoutubeWelcome />;
case MODALS.CREATE_CHANNEL: case MODALS.CREATE_CHANNEL:
return <ModalCreateChannel {...modalProps} />; return <ModalCreateChannel {...modalProps} />;
case MODALS.MOBILE_NAVIGATION:
return <ModalMobileNavigation {...modalProps} />;
case MODALS.SET_REFERRER: case MODALS.SET_REFERRER:
return <ModalSetReferrer {...modalProps} />; return <ModalSetReferrer {...modalProps} />;
case MODALS.REPOST: case MODALS.REPOST:

View file

@ -6,7 +6,7 @@ import Page from 'component/page';
import ClaimListDiscover from 'component/claimListDiscover'; import ClaimListDiscover from 'component/claimListDiscover';
import Button from 'component/button'; import Button from 'component/button';
import useHover from 'effects/use-hover'; import useHover from 'effects/use-hover';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
import analytics from 'analytics'; import analytics from 'analytics';
import HiddenNsfw from 'component/common/hidden-nsfw'; import HiddenNsfw from 'component/common/hidden-nsfw';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';

View file

@ -4,7 +4,6 @@ import { doSetContentHistoryItem } from 'redux/actions/content';
import { import {
doFetchFileInfo, doFetchFileInfo,
makeSelectFileInfoForUri, makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectMetadataForUri, makeSelectMetadataForUri,
makeSelectChannelForClaimUri, makeSelectChannelForClaimUri,
makeSelectClaimIsNsfw, makeSelectClaimIsNsfw,
@ -16,7 +15,6 @@ import { makeSelectFileRenderModeForUri } from 'redux/selectors/content';
import FilePage from './view'; import FilePage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
costInfo: makeSelectCostInfoForUri(props.uri)(state), costInfo: makeSelectCostInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state),
obscureNsfw: !selectShowMatureContent(state), obscureNsfw: !selectShowMatureContent(state),

View file

@ -15,12 +15,10 @@ import WaitUntilOnPage from 'component/common/wait-until-on-page';
import RecommendedContent from 'component/recommendedContent'; import RecommendedContent from 'component/recommendedContent';
import CommentsList from 'component/commentsList'; import CommentsList from 'component/commentsList';
import CommentCreate from 'component/commentCreate'; import CommentCreate from 'component/commentCreate';
import YoutubeBadge from 'component/youtubeBadge';
export const FILE_WRAPPER_CLASS = 'file-page__video-container'; export const FILE_WRAPPER_CLASS = 'file-page__video-container';
type Props = { type Props = {
claim: StreamClaim,
costInfo: ?{ includesData: boolean, cost: number }, costInfo: ?{ includesData: boolean, cost: number },
fileInfo: FileListItem, fileInfo: FileListItem,
uri: string, uri: string,
@ -86,13 +84,9 @@ class FilePage extends React.Component<Props> {
} }
renderFilePageLayout(uri: string, mode: string, cost: ?number) { renderFilePageLayout(uri: string, mode: string, cost: ?number) {
const { claim } = this.props;
const channelClaimId = claim.signing_channel ? claim.signing_channel.claim_id : null;
if (RENDER_MODES.FLOATING_MODES.includes(mode)) { if (RENDER_MODES.FLOATING_MODES.includes(mode)) {
return ( return (
<React.Fragment> <React.Fragment>
<YoutubeBadge channelClaimId={channelClaimId} includeSyncDate={false} />
<div className={FILE_WRAPPER_CLASS}> <div className={FILE_WRAPPER_CLASS}>
<FileRenderInitiator uri={uri} /> <FileRenderInitiator uri={uri} />
</div> </div>
@ -105,7 +99,6 @@ class FilePage extends React.Component<Props> {
if (RENDER_MODES.UNRENDERABLE_MODES.includes(mode)) { if (RENDER_MODES.UNRENDERABLE_MODES.includes(mode)) {
return ( return (
<React.Fragment> <React.Fragment>
<YoutubeBadge channelClaimId={channelClaimId} includeSyncDate={false} />
<FileTitle uri={uri} /> <FileTitle uri={uri} />
<FileRenderDownload uri={uri} isFree={cost === 0} /> <FileRenderDownload uri={uri} isFree={cost === 0} />
</React.Fragment> </React.Fragment>
@ -115,7 +108,6 @@ class FilePage extends React.Component<Props> {
if (RENDER_MODES.TEXT_MODES.includes(mode)) { if (RENDER_MODES.TEXT_MODES.includes(mode)) {
return ( return (
<React.Fragment> <React.Fragment>
<YoutubeBadge channelClaimId={channelClaimId} includeSyncDate={false} />
<FileTitle uri={uri} /> <FileTitle uri={uri} />
<FileRenderInitiator uri={uri} /> <FileRenderInitiator uri={uri} />
<FileRenderInline uri={uri} /> <FileRenderInline uri={uri} />
@ -125,7 +117,6 @@ class FilePage extends React.Component<Props> {
return ( return (
<React.Fragment> <React.Fragment>
<YoutubeBadge channelClaimId={channelClaimId} includeSyncDate={false} />
<FileRenderInitiator uri={uri} /> <FileRenderInitiator uri={uri} />
<FileRenderInline uri={uri} /> <FileRenderInline uri={uri} />
<FileTitle uri={uri} /> <FileTitle uri={uri} />
@ -152,29 +143,26 @@ class FilePage extends React.Component<Props> {
} }
return ( return (
<Page className="file-page"> <Page className="file-page" filePage>
<div className={classnames('section card-stack', `file-page__${renderMode}`)}> <div className={classnames('section card-stack', `file-page__${renderMode}`)}>
{this.renderFilePageLayout(uri, renderMode, costInfo ? costInfo.cost : null)} {this.renderFilePageLayout(uri, renderMode, costInfo ? costInfo.cost : null)}
<FileDescription uri={uri} />
<FileValues uri={uri} />
<FileDetails uri={uri} />
<Card
title={__('Leave a Comment')}
actions={
<div>
<CommentCreate uri={uri} />
<WaitUntilOnPage lastUpdateDate={this.lastReset}>
<CommentsList uri={uri} />
</WaitUntilOnPage>
</div>
}
/>
</div> </div>
<div className="section columns">
<div className="card-stack"> <RecommendedContent uri={uri} />
<FileDescription uri={uri} />
<FileValues uri={uri} />
<FileDetails uri={uri} />
<Card
title={__('Leave a Comment')}
actions={
<div>
<CommentCreate uri={uri} />
<WaitUntilOnPage lastUpdateDate={this.lastReset}>
<CommentsList uri={uri} />
</WaitUntilOnPage>
</div>
}
/>
</div>
<RecommendedContent uri={uri} />
</div>
</Page> </Page>
); );
} }

View file

@ -4,6 +4,7 @@ import {
selectIsFetchingNotifications, selectIsFetchingNotifications,
selectUnreadNotificationCount, selectUnreadNotificationCount,
} from 'redux/selectors/notifications'; } from 'redux/selectors/notifications';
import { doReadNotifications } from 'redux/actions/notifications';
import NotificationsPage from './view'; import NotificationsPage from './view';
const select = state => ({ const select = state => ({
@ -12,4 +13,6 @@ const select = state => ({
unreadCount: selectUnreadNotificationCount(state), unreadCount: selectUnreadNotificationCount(state),
}); });
export default connect(select)(NotificationsPage); export default connect(select, {
doReadNotifications,
})(NotificationsPage);

View file

@ -9,10 +9,12 @@ import Notification from 'component/notification';
type Props = { type Props = {
notifications: ?Array<Notification>, notifications: ?Array<Notification>,
fetching: boolean, fetching: boolean,
unreadCount: number,
doReadNotifications: () => void,
}; };
export default function NotificationsPage(props: Props) { export default function NotificationsPage(props: Props) {
const { notifications, fetching } = props; const { notifications, fetching, unreadCount, doReadNotifications } = props;
// Group sequential comment notifications if they are by the same author // Group sequential comment notifications if they are by the same author
let groupedCount = 1; let groupedCount = 1;
@ -59,6 +61,12 @@ export default function NotificationsPage(props: Props) {
} }
}, []); }, []);
React.useEffect(() => {
if (unreadCount > 0) {
doReadNotifications();
}
}, [unreadCount, doReadNotifications]);
return ( return (
<Page> <Page>
{fetching && ( {fetching && (

View file

@ -78,3 +78,14 @@
opacity: 1; opacity: 1;
} }
} }
@keyframes shadow-pulse {
0% {
background-color: rgba(37, 119, 97, 0.2);
box-shadow: 0 0 0 0px rgba(37, 119, 97, 0.2);
}
100% {
background-color: rgba(37, 119, 97, 0);
box-shadow: 0 0 0 35px rgba(37, 119, 97, 0);
}
}

View file

@ -97,6 +97,10 @@
} }
} }
.card__title-section--small {
padding: var(--spacing-s) var(--spacing-m);
}
.card__actions--inline { .card__actions--inline {
@extend .card__actions; @extend .card__actions;
margin-top: 0; margin-top: 0;
@ -181,10 +185,14 @@
} }
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
font-size: var(--font-body); font-size: var(--font-large);
} }
} }
.card__title--small {
font-size: var(--font-body);
}
.card__title-actions { .card__title-actions {
align-self: flex-start; align-self: flex-start;
padding: var(--spacing-m); padding: var(--spacing-m);

View file

@ -350,7 +350,7 @@
} }
@media (max-width: $breakpoint-medium) and (min-width: $breakpoint-small) { @media (max-width: $breakpoint-medium) and (min-width: $breakpoint-small) {
$width: calc((100vw - var(--side-nav-width) - (var(--spacing-l) * 4) - var(--spacing-m) * 2) / 3); $width: calc((100vw - var(--side-nav-width) - (var(--spacing-m) * 3)) / 3);
width: $width; width: $width;
@include handleClaimTileGifThumbnail($width); @include handleClaimTileGifThumbnail($width);

View file

@ -30,6 +30,10 @@
} }
} }
} }
@media (max-width: $breakpoint-medium) {
flex-direction: column;
}
} }
.file-render { .file-render {

View file

@ -24,19 +24,11 @@
padding-top: var(--mac-titlebar-height); padding-top: var(--mac-titlebar-height);
} }
.header--noauth-web {
.header__navigation-item--icon {
margin: 0 10px;
}
}
.header__contents { .header__contents {
max-width: var(--page-max-width); height: calc(var(--header-height));
height: calc(var(--header-height) - 1px);
display: flex; display: flex;
align-items: center; align-items: center;
margin: auto; padding: 0 var(--spacing-m);
padding: 0 var(--spacing-l);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
padding: var(--spacing-s); padding: var(--spacing-s);
@ -47,7 +39,6 @@
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
height: var(--height-input);
} }
.header__menu { .header__menu {
@ -65,29 +56,8 @@
} }
} }
.header__menu--mobile { .header__buttons {
display: none;
@media (max-width: $breakpoint-small) {
display: block;
margin-left: var(--spacing-m);
svg {
stroke: var(--color-text);
}
}
}
.header__menu--with-balance {
button:first-child {
margin-left: var(--spacing-l);
margin-right: var(--spacing-m);
}
}
.header__navigation-arrows {
display: flex; display: flex;
margin-right: var(--spacing-s);
} }
.header__navigation-item { .header__navigation-item {
@ -98,6 +68,7 @@
border-radius: var(--border-radius); border-radius: var(--border-radius);
color: var(--color-text); color: var(--color-text);
position: relative; position: relative;
font-weight: var(--font-weight-bold);
svg { svg {
stroke: var(--color-text); stroke: var(--color-text);
@ -118,7 +89,7 @@
width: var(--height-button); width: var(--height-button);
background-color: var(--color-header-button); background-color: var(--color-header-button);
border-radius: 1.5rem; border-radius: 1.5rem;
margin-left: var(--spacing-s); margin-right: var(--spacing-s);
&:hover { &:hover {
background-color: var(--color-primary-alt); background-color: var(--color-primary-alt);
@ -137,13 +108,24 @@
} }
} }
.header__navigation-item--icon {
@media (max-width: $breakpoint-small) {
margin-left: 0;
}
}
.header__navigation-item--lbry { .header__navigation-item--lbry {
font-weight: var(--font-weight-bold); padding: var(--spacing-s);
margin-right: var(--spacing-m);
.lbry-icon { .lbry-icon {
height: 2rem; height: var(--height-button);
width: 2rem; width: var(--height-button);
}
@media (max-width: $breakpoint-small) {
.button__label {
display: none;
}
} }
} }
@ -155,11 +137,6 @@
} }
} }
.header__navigation-item--balance {
margin: 0 var(--spacing-m);
font-weight: var(--font-weight-bold);
}
.header__navigation-dropdown { .header__navigation-dropdown {
@extend .menu__list--header; @extend .menu__list--header;
padding: 0; padding: 0;
@ -195,3 +172,9 @@
margin: 0 var(--spacing-m); margin: 0 var(--spacing-m);
} }
} }
.header__center {
display: flex;
justify-content: center;
width: 100%;
}

View file

@ -13,27 +13,68 @@
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
min-height: calc(100vh - var(--header-height)); min-height: calc(100vh - var(--header-height));
max-width: var(--page-max-width);
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: var(--header-height); margin-top: var(--header-height);
padding: var(--spacing-l); // Unfortunately this is coupled with .claim-preview--tile width calculation padding: var(--spacing-l); // Unfortunately this is coupled with .claim-preview--tile width calculation
padding-left: 0;
padding-right: 0;
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
padding: var(--spacing-s); padding: var(--spacing-xs);
padding-top: var(--spacing-m);
} }
@media (min-width: $breakpoint-large) {
width: 100%;
}
}
.main-wrapper__inner--filepage {
padding: 0;
padding-top: var(--spacing-s);
} }
.main { .main {
position: relative; position: relative;
width: calc(100% - var(--side-nav-width) - var(--spacing-l)); width: calc(100% - var(--side-nav-width) - var(--spacing-l));
max-width: var(--page-max-width);
z-index: 0;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-medium) {
width: 100%; width: 100%;
margin-right: 0; margin-right: var(--spacing-s);
margin-left: 0; margin-left: var(--spacing-s);
}
}
.main--file-page {
width: 100%;
max-width: var(--page-max-width--filepage);
margin-left: auto;
margin-right: auto;
display: flex;
padding-left: var(--spacing-m);
padding-right: var(--spacing-m);
> :first-child {
flex: 1;
margin-right: var(--spacing-m);
}
.file-page__recommended {
width: 25rem;
height: 0%;
}
@media (max-width: $breakpoint-medium) {
flex-direction: column;
> :first-child {
margin-right: 0;
}
} }
} }

View file

@ -46,20 +46,6 @@
box-shadow: none; box-shadow: none;
border: none; border: none;
} }
.navigation {
width: 100%;
display: block;
padding: var(--spacing-l);
.navigation-links:not(.navigation-links--small) {
.navigation-link {
font-size: var(--font-large);
border-bottom: 1px solid var(--color-gray-3);
padding: var(--spacing-m) 0;
}
}
}
} }
} }

View file

@ -1,22 +1,128 @@
.navigation { .navigation {
width: var(--side-nav-width); width: var(--side-nav-width);
font-size: var(--font-body);
margin-left: var(--spacing-main-padding);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
display: none; display: none;
margin-left: 0;
} }
} }
.navigation + .ads-wrapper { .navigation--filepage {
margin-top: var(--spacing-l); z-index: 4;
position: fixed;
top: var(--header-height);
left: 0;
height: calc(100vh - var(--header-height));
width: var(--side-nav-width--large);
overflow-y: scroll;
margin-top: 0;
padding: var(--spacing-l);
padding-top: var(--spacing-s);
padding-left: 0;
border-top: 1px solid var(--color-border);
background-color: var(--color-card-background);
box-shadow: var(--card-box-shadow);
.navigation-link {
padding-left: var(--spacing-m);
}
}
.navigation--micro {
width: var(--side-nav-width--micro);
} }
.navigation__secondary { .navigation__secondary {
margin-top: var(--spacing-l); margin-top: var(--spacing-l);
} }
.navigation-link {
display: block;
position: relative;
transition: color 0.2s;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--color-navigation-link);
margin-bottom: var(--spacing-s);
padding-left: var(--spacing-s);
.icon {
height: 1.5rem;
width: 1.5rem;
stroke: var(--color-gray-5);
}
.button__content {
padding: var(--spacing-s);
justify-content: flex-start;
flex-direction: column;
}
.button__label {
font-size: var(--font-small);
}
&:hover {
@extend .navigation-link--extra;
color: var(--color-gray-5);
.icon {
stroke: var(--color-gray-5);
}
}
@media (min-width: $breakpoint-medium) {
text-align: left;
margin-bottom: 0;
.icon {
height: 1rem;
width: 1rem;
}
.button__content {
flex-direction: row;
}
.button__label {
margin-top: 0;
font-size: var(--font-body);
font-weight: var(--font-weight-bold);
}
&:focus {
box-shadow: none;
}
}
}
.navigation-link--active {
background-color: var(--color-primary-alt);
border-radius: var(--border-radius);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
color: var(--color-primary);
.icon {
stroke: var(--color-primary);
}
}
.navigation-link--pulse {
overflow: visible;
.icon {
animation: shadow-pulse 2.5s infinite;
}
}
.navigation-link--extra {
border-radius: var(--border-radius);
border-top-left-radius: 0;
border-bottom-left-radius: 0;
background-color: var(--color-gray-1);
}
.navigation-links { .navigation-links {
@extend .ul--no-style; @extend .ul--no-style;
flex-direction: column; flex-direction: column;
@ -24,64 +130,88 @@
list-style: none; list-style: none;
} }
.navigation-links--small { .navigation-links--relative {
@extend .navigation-links; @extend .navigation-links;
font-size: var(--font-small); margin-right: var(--spacing-m);
.navigation-link {
padding-left: var(--spacing-s);
}
} }
.navigation-links__inline { .navigation-links--micro {
margin-left: calc(var(--spacing-m) + var(--spacing-s));
}
.navigation-link__wrapper {
margin: var(--spacing-xxs) 0;
}
.navigation-link {
display: block;
position: relative;
text-align: left;
transition: color 0.2s;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: var(--color-navigation-link);
margin-top: var(--spacing-s);
.icon { .icon {
stroke: var(--color-link-icon); height: 1.5rem;
width: 1.5rem;
} }
&:hover, .button__content {
&.navigation-link--active { padding: var(--spacing-s);
color: var(--color-link-active); justify-content: flex-start;
flex-direction: column;
}
.button__label {
font-size: var(--font-xsmall);
margin-left: 0;
margin-top: var(--spacing-xs);
}
.navigation-link {
margin-bottom: var(--spacing-xs);
padding-left: 0;
}
@media (max-width: $breakpoint-small) {
display: none;
}
}
.navigation-links--absolute {
@extend .navigation-links;
.navigation-link {
margin-bottom: 0;
.icon { .icon {
stroke: var(--color-link-active); height: 1rem;
width: 1rem;
}
.button__content {
flex-direction: row;
}
.button__label {
margin-top: 0;
font-size: var(--font-body);
font-weight: var(--font-weight-bold);
}
&:focus {
box-shadow: none;
} }
} }
}
&:focus { .navigation-links--small {
box-shadow: none; @extend .navigation-links;
margin-right: 0;
padding-right: 0;
font-size: var(--font-small);
.button__content {
align-items: flex-start;
} }
} }
.navigation-link--pulse { .navigation__overlay {
border-radius: var(--border-radius); position: fixed;
overflow: visible; width: 100vw;
height: 100vh;
.icon { opacity: 0.3;
border-radius: 50%; background-color: black;
animation: shadow-pulse 2.5s infinite; z-index: 3;
} left: 0;
} top: var(--header-height);
@keyframes shadow-pulse {
0% {
background-color: rgba(37, 119, 97, 0.2);
box-shadow: 0 0 0 0px rgba(37, 119, 97, 0.2);
}
100% {
background-color: rgba(37, 119, 97, 0);
box-shadow: 0 0 0 35px rgba(37, 119, 97, 0);
}
} }

View file

@ -51,3 +51,20 @@
margin-bottom: 0; margin-bottom: 0;
margin-top: 0; margin-top: 0;
} }
.notification__bubble {
height: 1.5rem;
width: 1.5rem;
border-radius: 50%;
background-color: var(--color-notification);
position: absolute;
top: -0.5rem;
right: -0.5rem;
color: white;
font-size: var(--font-small);
font-weight: bold;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}

View file

@ -7,10 +7,13 @@
z-index: 1; z-index: 1;
font-size: var(--font-small); font-size: var(--font-small);
height: var(--height-input); height: var(--height-input);
max-width: 20rem; max-width: 30rem;
margin-left: var(--spacing-s);
margin-right: var(--spacing-s);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
max-width: none; max-width: none;
margin: 0;
} }
> .icon { > .icon {

View file

@ -12,6 +12,7 @@
--color-warning: #fff58c; --color-warning: #fff58c;
--color-cost: #ffd580; --color-cost: #ffd580;
--color-focus: #93cff2; --color-focus: #93cff2;
--color-notification: #f02849;
--color-black: #111; --color-black: #111;
--color-white: #fdfdfd; --color-white: #fdfdfd;
@ -47,7 +48,7 @@
--color-input-color: #111111; --color-input-color: #111111;
--color-input-label: var(--color-gray-5); --color-input-label: var(--color-gray-5);
--color-input-placeholder: #212529; --color-input-placeholder: #212529;
--color-input-bg: #f2f2f2; --color-input-bg: var(--color-gray-1);
--color-input-bg-copyable: #434b53; --color-input-bg-copyable: #434b53;
--color-input-border: var(--color-border); --color-input-border: var(--color-border);
--color-input-border-active: var(--color-secondary); --color-input-border-active: var(--color-secondary);

View file

@ -53,6 +53,7 @@ p {
ul, ul,
ol { ol {
li { li {
position: relative;
list-style-position: outside; list-style-position: outside;
margin: var(--spacing-xs) var(--spacing-m); margin: var(--spacing-xs) var(--spacing-m);
margin-bottom: 0; margin-bottom: 0;
@ -312,16 +313,8 @@ textarea {
} }
} }
.notification__bubble { .mobile-hidden {
height: 1.5rem; @media (max-width: $breakpoint-small) {
width: 1.5rem; display: none !important;
border-radius: 50%; }
background-color: #f02849;
position: absolute;
top: -0.5rem;
right: -0.5rem;
color: white;
font-size: var(--font-small);
font-weight: bold;
line-height: 1;
} }

View file

@ -5,6 +5,7 @@ $spacing-width: 36px;
$breakpoint-xsmall: 600px; $breakpoint-xsmall: 600px;
$breakpoint-small: 900px; $breakpoint-small: 900px;
$breakpoint-medium: 1150px; $breakpoint-medium: 1150px;
$breakpoint-large: 1600px;
:root { :root {
--border-radius: 5px; --border-radius: 5px;
@ -44,10 +45,13 @@ $breakpoint-medium: 1150px;
--font-heading: 2.94rem; --font-heading: 2.94rem;
// Width & spacing // Width & spacing
--page-max-width: 1420px; --page-max-width: 1280px;
--page-max-width--filepage: 1700px;
--mac-titlebar-height: 1.5rem; --mac-titlebar-height: 1.5rem;
--mobile: 600px; --mobile: 600px;
--side-nav-width: 170px; --side-nav-width: 200px;
--side-nav-width--micro: 100px;
--side-nav-width--large: 250px;
--spacing-main-padding: var(--spacing-xl); --spacing-main-padding: var(--spacing-xl);
--floating-viewer-width: 32rem; --floating-viewer-width: 32rem;

View file

@ -3,7 +3,7 @@
--color-link-icon: var(--color-gray-4); --color-link-icon: var(--color-gray-4);
--color-link-active: var(--color-primary); --color-link-active: var(--color-primary);
--color-navigation-link: var(--color-gray-5); --color-navigation-link: var(--color-gray-5);
--color-header-button: #f7f7f7; --color-header-button: var(--color-button-alt-bg);
--color-button-border: var(--color-gray-3); --color-button-border: var(--color-gray-3);
// Color // Color

View file

@ -3,7 +3,7 @@ import React from 'react';
import Nag from 'component/common/nag'; import Nag from 'component/common/nag';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import Button from 'component/button'; import Button from 'component/button';
import useIsMobile from 'effects/use-is-mobile'; import { useIsMobile } from 'effects/use-screensize';
type Props = { type Props = {
onClose: () => void, onClose: () => void,
@ -41,8 +41,7 @@ export default function NagDegradedPerformance(props: Props) {
), ),
}} }}
> >
lbry.tv collects usage information for itself only (%more_information%). Want control over lbry.tv collects usage information for itself only (%more_information%). Want control over this and more?
this and more?
</I18nMessage> </I18nMessage>
} }
actionText={__('Get The App')} actionText={__('Get The App')}