Right Click to Navigate History (#3547)

* merge && backwards functionality working (kind of) WIP

* wip - need to fix forward and backwards buttons

* history works well but only for channel related pages - need to add title update hook for other pages

* moved useEffect to router

* renamed buttonNavigation -> navigationButton

* removed unused history

* fixed issue with lbry.tv

* disable button if no entries

* added max size for history

* set correct margin-top for nav button dropdown

* cleanup

Co-authored-by: Sean Yesmunt <sean@lbry.io>
This commit is contained in:
Dalton Hill 2020-02-03 09:19:15 -06:00 committed by GitHub
parent 8ec2032c7a
commit 6d88d87ff2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 208 additions and 48 deletions

View file

@ -915,12 +915,7 @@
"Support %amount% LBC": "Support %amount% LBC", "Support %amount% LBC": "Support %amount% LBC",
"You deposited %amount% LBC as a support!": "You deposited %amount% LBC as a support!", "You deposited %amount% LBC as a support!": "You deposited %amount% LBC as a support!",
"LBRY Link": "LBRY Link", "LBRY Link": "LBRY Link",
"Publish to %uri%": "Publish to %uri%",
"Your wallet": "Your wallet", "Your wallet": "Your wallet",
"Publish a file, or create a channel": "Publish a file, or create a channel", "Publish a file, or create a channel": "Publish a file, or create a channel",
"Your account": "Your account", "Your account": "Your account"
"Channel profile picture": "Channel profile picture",
"refreshing the app": "refreshing the app",
"Follower": "Follower",
"%repost_channel_link% reposted": "%repost_channel_link% reposted"
} }

View file

@ -11,6 +11,7 @@ import WunderBar from 'component/wunderbar';
import Icon from 'component/common/icon'; 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';
// @if TARGET='app' // @if TARGET='app'
import { IS_MAC } from 'component/app/view'; import { IS_MAC } from 'component/app/view';
// @endif // @endif
@ -18,7 +19,15 @@ import { IS_MAC } from 'component/app/view';
type Props = { type Props = {
balance: string, balance: string,
roundedBalance: number, roundedBalance: number,
history: { push: string => void, goBack: () => void, goForward: () => void, location: { pathname: string } }, history: {
entities: {}[],
goBack: () => void,
goForward: () => void,
index: number,
length: number,
location: { pathname: string },
push: string => void,
},
currentTheme: string, currentTheme: string,
automaticDarkModeEnabled: boolean, automaticDarkModeEnabled: boolean,
setClientSetting: (string, boolean | string) => void, setClientSetting: (string, boolean | string) => void,
@ -105,21 +114,8 @@ const Header = (props: Props) => {
{/* @if TARGET='app' */} {/* @if TARGET='app' */}
{!authHeader && ( {!authHeader && (
<div className="header__navigation-arrows"> <div className="header__navigation-arrows">
<Button <NavigationButton isBackward history={history} />
className="header__navigation-item header__navigation-item--back" <NavigationButton isBackward={false} history={history} />
description={__('Navigate back')}
onClick={() => history.goBack()}
icon={ICONS.ARROW_LEFT}
iconSize={18}
/>
<Button
className="header__navigation-item header__navigation-item--forward"
description={__('Navigate forward')}
onClick={() => history.goForward()}
icon={ICONS.ARROW_RIGHT}
iconSize={18}
/>
</div> </div>
)} )}
{/* @endif */} {/* @endif */}

View file

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

View file

@ -0,0 +1,91 @@
// @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>
<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

@ -2,10 +2,31 @@ import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'lbryinc'; import { selectUserVerifiedEmail } from 'lbryinc';
import { selectScrollStartingPosition } from 'redux/selectors/app'; import { selectScrollStartingPosition } from 'redux/selectors/app';
import Router from './view'; import Router from './view';
import { normalizeURI, makeSelectTitleForUri } from 'lbry-redux';
const select = state => ({ const select = state => {
currentScroll: selectScrollStartingPosition(state), const { pathname, hash } = state.router.location;
isAuthenticated: selectUserVerifiedEmail(state), const urlPath = pathname + hash;
}); // Remove the leading "/" added by the browser
const path = urlPath.slice(1).replace(/:/g, '#');
let uri;
try {
uri = normalizeURI(path);
} catch (e) {
const match = path.match(/[#/:]/);
if (!path.startsWith('$/') && match && match.index) {
uri = `lbry://${path.slice(0, match.index)}`;
}
}
return {
uri,
title: makeSelectTitleForUri(uri)(state),
currentScroll: selectScrollStartingPosition(state),
isAuthenticated: selectUserVerifiedEmail(state),
};
};
export default connect(select)(Router); export default connect(select)(Router);

View file

@ -29,6 +29,8 @@ import SignInPage from 'page/signIn';
import SignInVerifyPage from 'page/signInVerify'; import SignInVerifyPage from 'page/signInVerify';
import ChannelsPage from 'page/channels'; import ChannelsPage from 'page/channels';
import EmbedWrapperPage from 'page/embedWrapper'; import EmbedWrapperPage from 'page/embedWrapper';
import { parseURI } from 'lbry-redux';
import { SITE_TITLE } from 'config';
// Tell the browser we are handling scroll restoration // Tell the browser we are handling scroll restoration
if ('scrollRestoration' in history) { if ('scrollRestoration' in history) {
@ -61,6 +63,19 @@ type Props = {
currentScroll: number, currentScroll: number,
location: { pathname: string, search: string }, location: { pathname: string, search: string },
isAuthenticated: boolean, isAuthenticated: boolean,
history: {
entries: { title: string }[],
goBack: () => void,
goForward: () => void,
index: number,
length: number,
location: { pathname: string },
push: string => void,
state: {},
replaceState: ({}, string, string) => void,
},
uri: string,
title: string,
}; };
function AppRouter(props: Props) { function AppRouter(props: Props) {
@ -68,7 +83,37 @@ function AppRouter(props: Props) {
currentScroll, currentScroll,
location: { pathname }, location: { pathname },
isAuthenticated, isAuthenticated,
history,
uri,
title,
} = props; } = props;
const { entries } = history;
const entryIndex = history.index;
useEffect(() => {
if (uri) {
const { channelName, streamName } = parseURI(uri);
if (typeof title !== 'undefined' && title !== '') {
document.title = title;
} else if (streamName) {
document.title = streamName;
} else if (channelName) {
document.title = channelName;
} else {
document.title = IS_WEB ? SITE_TITLE : 'LBRY';
}
} else {
document.title = IS_WEB ? SITE_TITLE : 'LBRY';
}
// @if TARGET='app'
entries[entryIndex].title = document.title;
// @endif
return () => {
document.title = IS_WEB ? SITE_TITLE : 'LBRY';
};
}, [entries, entryIndex, title, uri]);
useEffect(() => { useEffect(() => {
window.scrollTo(0, currentScroll); window.scrollTo(0, currentScroll);

View file

@ -1,12 +1,10 @@
// @flow // @flow
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { parseURI } from 'lbry-redux';
import BusyIndicator from 'component/common/busy-indicator'; import BusyIndicator from 'component/common/busy-indicator';
import ChannelPage from 'page/channel'; import ChannelPage from 'page/channel';
import FilePage from 'page/file'; import FilePage from 'page/file';
import Page from 'component/page'; import Page from 'component/page';
import Button from 'component/button'; import Button from 'component/button';
import { SITE_TITLE } from 'config';
import Card from 'component/common/card'; import Card from 'component/common/card';
type Props = { type Props = {
@ -24,8 +22,7 @@ type Props = {
}; };
function ShowPage(props: Props) { function ShowPage(props: Props) {
const { isResolvingUri, resolveUri, uri, claim, blackListedOutpoints, location, title, claimIsMine } = props; const { isResolvingUri, resolveUri, uri, claim, blackListedOutpoints, location, claimIsMine } = props;
const { channelName, streamName } = parseURI(uri);
const signingChannel = claim && claim.signing_channel; const signingChannel = claim && claim.signing_channel;
const canonicalUrl = claim && claim.canonical_url; const canonicalUrl = claim && claim.canonical_url;
const claimExists = claim !== null && claim !== undefined; const claimExists = claim !== null && claim !== undefined;
@ -46,22 +43,6 @@ function ShowPage(props: Props) {
} }
}, [resolveUri, isResolvingUri, canonicalUrl, uri, claimExists, haventFetchedYet]); }, [resolveUri, isResolvingUri, canonicalUrl, uri, claimExists, haventFetchedYet]);
useEffect(() => {
if (title) {
document.title = title;
} else if (streamName) {
document.title = streamName;
} else if (channelName) {
document.title = channelName;
} else {
document.title = IS_WEB ? SITE_TITLE : 'LBRY';
}
return () => {
document.title = IS_WEB ? SITE_TITLE : 'LBRY';
};
}, [title, channelName, streamName]);
let innerContent = ''; let innerContent = '';
if (!claim || (claim && !claim.name)) { if (!claim || (claim && !claim.name)) {

View file

@ -147,3 +147,29 @@
.header__navigation-item--balance { .header__navigation-item--balance {
margin: 0 var(--spacing-medium); margin: 0 var(--spacing-medium);
} }
.header__navigation-dropdown {
@extend .menu__list--header;
padding: 0;
position: absolute;
list-style-type: none;
background-color: var(--color-header-background);
}
.header__navigation-button {
margin: 0;
padding: var(--spacing-miniscule) var(--spacing-medium);
display: flex;
align-items: center;
&:hover {
cursor: pointer;
background-color: var(--color-menu-background--active);
}
}
.header__navigation-button-help {
@extend .help;
margin-top: 0;
margin-left: var(--spacing-small);
}

View file

@ -55,6 +55,8 @@
margin-left: calc(var(--spacing-medium) * -1); margin-left: calc(var(--spacing-medium) * -1);
box-shadow: var(--card-box-shadow); box-shadow: var(--card-box-shadow);
animation: menu-animate-in var(--animation-duration) var(--animation-style); animation: menu-animate-in var(--animation-duration) var(--animation-style);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
} }
.menu__link { .menu__link {

View file

@ -7,7 +7,7 @@ import { createFilter, createBlacklistFilter } from 'redux-persist-transform-fil
import localForage from 'localforage'; import localForage from 'localforage';
import { createStore, applyMiddleware, compose } from 'redux'; import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
import { createHashHistory, createBrowserHistory } from 'history'; import { createMemoryHistory, createBrowserHistory } from 'history';
import { routerMiddleware } from 'connected-react-router'; import { routerMiddleware } from 'connected-react-router';
import createRootReducer from './reducers'; import createRootReducer from './reducers';
import { buildSharedStateMiddleware, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux'; import { buildSharedStateMiddleware, ACTIONS as LBRY_REDUX_ACTIONS } from 'lbry-redux';
@ -96,7 +96,7 @@ const persistOptions = {
let history; let history;
// @if TARGET='app' // @if TARGET='app'
history = createHashHistory(); history = createMemoryHistory();
// @endif // @endif
// @if TARGET='web' // @if TARGET='web'
history = createBrowserHistory(); history = createBrowserHistory();