From 339945018d59bc0f722558b6932719d1e73170b6 Mon Sep 17 00:00:00 2001 From: Jeremy Kauffman Date: Wed, 30 Aug 2017 08:48:32 -0400 Subject: [PATCH] Fix bugs in history navigation in stack ordering and loss of scrolling and refactor history --- ui/js/actions/app.js | 151 +---------------- ui/js/actions/file_info.js | 3 +- ui/js/actions/navigation.js | 104 ++++++++++++ ui/js/actions/search.js | 4 +- ui/js/component/app/index.js | 6 +- ui/js/component/app/view.jsx | 10 ++ ui/js/component/fileActions/index.js | 3 +- ui/js/component/fileCard/index.js | 2 +- ui/js/component/fileListSearch/index.js | 2 +- ui/js/component/fileTile/index.js | 2 +- ui/js/component/header/index.js | 11 +- ui/js/component/nsfwOverlay/index.js | 2 +- ui/js/component/rewardLink/index.js | 2 +- ui/js/component/router/index.js | 5 +- ui/js/component/snackBar/index.js | 3 +- ui/js/component/snackBar/view.jsx | 4 +- ui/js/component/subHeader/index.js | 4 +- ui/js/component/userVerify/index.js | 2 +- ui/js/component/video/index.js | 2 +- ui/js/component/wunderbar/index.js | 2 +- ui/js/constants/action_types.js | 9 +- ui/js/main.js | 21 +-- ui/js/modal/modalCreditIntro/index.js | 3 +- ui/js/modal/modalInsufficientCredits/index.js | 3 +- ui/js/modal/modalRemoveFile/index.js | 3 +- ui/js/modal/modalRouter/index.js | 7 +- ui/js/page/auth/index.js | 4 +- ui/js/page/channel/index.js | 4 +- ui/js/page/file/index.js | 2 +- ui/js/page/fileListDownloaded/index.js | 2 +- ui/js/page/fileListPublished/index.js | 2 +- ui/js/page/help/index.js | 2 +- ui/js/page/publish/index.js | 2 +- ui/js/page/rewards/index.js | 2 +- ui/js/page/search/index.js | 2 +- ui/js/reducers/app.js | 77 --------- ui/js/reducers/navigation.js | 85 ++++++++++ ui/js/selectors/app.js | 153 ------------------ ui/js/selectors/claims.js | 2 +- ui/js/selectors/cost_info.js | 2 +- ui/js/selectors/navigation.js | 129 +++++++++++++++ ui/js/selectors/search.js | 2 +- ui/js/selectors/wallet.js | 1 - ui/js/store.js | 6 +- 44 files changed, 399 insertions(+), 450 deletions(-) create mode 100644 ui/js/actions/navigation.js create mode 100644 ui/js/reducers/navigation.js create mode 100644 ui/js/selectors/navigation.js diff --git a/ui/js/actions/app.js b/ui/js/actions/app.js index ec2af78f0..3d81d6c69 100644 --- a/ui/js/actions/app.js +++ b/ui/js/actions/app.js @@ -5,18 +5,10 @@ import { selectUpgradeDownloadPath, selectUpgradeDownloadItem, selectUpgradeFilename, - selectPageTitle, - selectCurrentPage, - selectCurrentParams, - selectHistoryBack, - selectHistoryForward, } from "selectors/app"; -import { doSearch } from "actions/search"; import { doFetchDaemonSettings } from "actions/settings"; import { doAuthenticate } from "actions/user"; import { doFileList } from "actions/file_info"; -import { toQueryString } from "util/query_params"; -import { parseQueryParams } from "util/query_params"; const { remote, ipcRenderer, shell } = require("electron"); const path = require("path"); @@ -24,130 +16,6 @@ const { download } = remote.require("electron-dl"); const fs = remote.require("fs"); const { lbrySettings: config } = require("../../../app/package.json"); -export function doNavigate(path, params = {}, options = {}) { - return function(dispatch, getState) { - if (!path) { - return; - } - - let url = path; - if (params) url = `${url}?${toQueryString(params)}`; - - dispatch(doChangePath(url)); - - const state = getState(); - const pageTitle = selectPageTitle(state); - const historyState = history.state; - - dispatch( - doHistoryPush({ params, page: historyState.page + 1 }, pageTitle, url) - ); - }; -} - -export function doAuthNavigate(pathAfterAuth = null, params = {}) { - return function(dispatch, getState) { - if (pathAfterAuth) { - dispatch({ - type: types.CHANGE_AFTER_AUTH_PATH, - data: { - path: `${pathAfterAuth}?${toQueryString(params)}`, - }, - }); - } - dispatch(doNavigate("/auth")); - }; -} - -export function doChangePath(path, options = {}) { - return function(dispatch, getState) { - dispatch({ - type: types.CHANGE_PATH, - data: { - path, - }, - }); - - const state = getState(); - const pageTitle = selectPageTitle(state); - const scrollY = options.scrollY; - - window.document.title = pageTitle; - - if (scrollY) window.scrollTo(0, scrollY); - else window.scrollTo(0, 0); - - const currentPage = selectCurrentPage(state); - if (currentPage === "search") { - const params = selectCurrentParams(state); - dispatch(doSearch(params.query)); - } - }; -} - -export function doHistoryBack() { - return function(dispatch, getState) { - // Get back history from stack - const back = selectHistoryBack(getState()); - - if (back) { - // Set location - dispatch(doChangePath(back.location)); - - dispatch({ - type: types.HISTORY_NAVIGATE, - data: { page: back }, - }); - } - }; -} - -export function doHistoryForward() { - return function(dispatch, getState) { - // Get forward history from stack - const forward = selectHistoryForward(getState()); - - if (forward) { - // Set location - dispatch(doChangePath(forward.location)); - - dispatch({ - type: types.HISTORY_NAVIGATE, - data: { page: forward }, - }); - } - }; -} - -export function doHistoryPush(currentState, title, relativeUrl) { - return function(dispatch, getState) { - title += " - LBRY"; - history.pushState(currentState, title, `#${relativeUrl}`); - dispatch({ - type: types.HISTORY_NAVIGATE, - data: { - location: relativeUrl, - }, - }); - }; -} - -export function doRecordScroll(scroll) { - return function(dispatch, getState) { - const state = getState(); - const historyState = history.state; - - if (!historyState) return; - - historyState.scrollY = scroll; - history.replaceState( - historyState, - document.title, - `#${state.app.currentPath}` - ); - }; -} - export function doOpenModal(modal) { return { type: types.OPEN_MODAL, @@ -305,25 +173,8 @@ export function doAlertError(errorList) { export function doDaemonReady() { return function(dispatch, getState) { - const path = window.location.hash || "#/discover"; - const params = parseQueryParams(path.split("?")[1] || ""); - - // Get first page - const page = { - index: 0, - location: path.replace(/^#/, ""), - }; - - history.replaceState( - { params, is_first_page: true, page: 1 }, - document.title, - `${path}` - ); dispatch(doAuthenticate()); - dispatch({ - type: types.DAEMON_READY, - data: { page }, - }); + dispatch({ type: types.DAEMON_READY }); dispatch(doFetchDaemonSettings()); dispatch(doFileList()); }; diff --git a/ui/js/actions/file_info.js b/ui/js/actions/file_info.js index c69db230c..7d9d102c6 100644 --- a/ui/js/actions/file_info.js +++ b/ui/js/actions/file_info.js @@ -12,7 +12,8 @@ import { selectUrisLoading, selectTotalDownloadProgress, } from "selectors/file_info"; -import { doCloseModal, doHistoryBack } from "actions/app"; +import { doCloseModal } from "actions/app"; +import { doHistoryBack } from "actions/navigation"; import setProgressBar from "util/setProgressBar"; import batchActions from "util/batchActions"; diff --git a/ui/js/actions/navigation.js b/ui/js/actions/navigation.js new file mode 100644 index 000000000..b34a2e876 --- /dev/null +++ b/ui/js/actions/navigation.js @@ -0,0 +1,104 @@ +import * as types from "constants/action_types"; +import { + selectPageTitle, + selectCurrentPage, + selectCurrentParams, + selectHistoryStack, + selectHistoryIndex, +} from "selectors/navigation"; +import { doSearch } from "actions/search"; +import { toQueryString } from "util/query_params"; + +export function doNavigate(path, params = {}, options = {}) { + return function(dispatch, getState) { + if (!path) { + return; + } + + let url = path; + if (params && Object.values(params).length) { + url += "?" + toQueryString(params); + } + + dispatch(doChangePath(url, options)); + + const pageTitle = selectPageTitle(getState()) + " - LBRY"; + + dispatch({ + type: types.HISTORY_NAVIGATE, + data: { url, index: options.index }, + }); + }; +} + +export function doAuthNavigate(pathAfterAuth = null, params = {}) { + return function(dispatch, getState) { + if (pathAfterAuth) { + dispatch({ + type: types.CHANGE_AFTER_AUTH_PATH, + data: { + path: `${pathAfterAuth}?${toQueryString(params)}`, + }, + }); + } + dispatch(doNavigate("/auth")); + }; +} + +export function doChangePath(path, options = {}) { + return function(dispatch, getState) { + dispatch({ + type: types.CHANGE_PATH, + data: { + path, + }, + }); + + const state = getState(); + const scrollY = options.scrollY; + + //I wasn't seeing it scroll to the proper position without this -- possibly because the page isn't fully rendered? Not sure - Jeremy + setTimeout(() => { + window.scrollTo(0, scrollY ? scrollY : 0); + }, 100); + + const currentPage = selectCurrentPage(state); + if (currentPage === "search") { + const params = selectCurrentParams(state); + dispatch(doSearch(params.query)); + } + }; +} + +export function doHistoryTraverse(dispatch, state, modifier) { + const stack = selectHistoryStack(state), + index = selectHistoryIndex(state) + modifier; + + if (index >= 0 && index < stack.length) { + const historyItem = stack[index]; + return dispatch( + doNavigate(historyItem.path, {}, { scrollY: historyItem.scrollY, index }) + ); + } +} + +export function doHistoryBack() { + return function(dispatch, getState) { + return doHistoryTraverse(dispatch, getState(), -1); + }; +} + +export function doHistoryForward() { + return function(dispatch, getState) { + return doHistoryTraverse(dispatch, getState(), 1); + }; +} + +export function doRecordScroll(scroll) { + return function(dispatch, getState) { + dispatch({ + type: types.WINDOW_SCROLLED, + data: { scrollY: scroll }, + }); + }; +} diff --git a/ui/js/actions/search.js b/ui/js/actions/search.js index a693dc26a..2d6be08ee 100644 --- a/ui/js/actions/search.js +++ b/ui/js/actions/search.js @@ -2,8 +2,8 @@ import * as types from "constants/action_types"; import lbryuri from "lbryuri"; import lighthouse from "lighthouse"; import { doResolveUri } from "actions/content"; -import { doNavigate, doHistoryPush } from "actions/app"; -import { selectCurrentPage } from "selectors/app"; +import { doNavigate } from "actions/navigation"; +import { selectCurrentPage } from "selectors/navigation"; import batchActions from "util/batchActions"; export function doSearch(query) { diff --git a/ui/js/component/app/index.js b/ui/js/component/app/index.js index fdacf035d..38527d94b 100644 --- a/ui/js/component/app/index.js +++ b/ui/js/component/app/index.js @@ -1,16 +1,18 @@ import React from "react"; import { connect } from "react-redux"; +import { selectPageTitle } from "selectors/navigation"; +import { selectUser } from "selectors/user"; import { doCheckUpgradeAvailable, doAlertError, - doRecordScroll, } from "actions/app"; +import { doRecordScroll } from "actions/navigation"; import { doFetchRewardedContent } from "actions/content"; import { doUpdateBalance } from "actions/wallet"; -import { selectUser } from "selectors/user"; import App from "./view"; const select = (state, props) => ({ + pageTitle: selectPageTitle(state), user: selectUser(state), }); diff --git a/ui/js/component/app/view.jsx b/ui/js/component/app/view.jsx index 32cb74247..e3c2465de 100644 --- a/ui/js/component/app/view.jsx +++ b/ui/js/component/app/view.jsx @@ -30,12 +30,22 @@ class App extends React.PureComponent { this.scrollListener = () => this.props.recordScroll(window.scrollY); window.addEventListener("scroll", this.scrollListener); + + this.setTitleFromProps(this.props); } componentWillUnmount() { window.removeEventListener("scroll", this.scrollListener); } + componentWillReceiveProps(props) { + this.setTitleFromProps(props); + } + + setTitleFromProps(props) { + window.document.title = props.pageTitle; + } + render() { return (
diff --git a/ui/js/component/fileActions/index.js b/ui/js/component/fileActions/index.js index 4311823d1..759abee47 100644 --- a/ui/js/component/fileActions/index.js +++ b/ui/js/component/fileActions/index.js @@ -1,13 +1,12 @@ import React from "react"; import { connect } from "react-redux"; -import { selectPlatform } from "selectors/app"; +import { selectPlatform, selectCurrentModal } from "selectors/app"; import { makeSelectFileInfoForUri, makeSelectDownloadingForUri, makeSelectLoadingForUri, } from "selectors/file_info"; import { makeSelectIsAvailableForUri } from "selectors/availability"; -import { selectCurrentModal } from "selectors/app"; import { makeSelectCostInfoForUri } from "selectors/cost_info"; import { doCloseModal, doOpenModal } from "actions/app"; import { doFetchAvailability } from "actions/availability"; diff --git a/ui/js/component/fileCard/index.js b/ui/js/component/fileCard/index.js index 68ede91e1..d933736e8 100644 --- a/ui/js/component/fileCard/index.js +++ b/ui/js/component/fileCard/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doResolveUri } from "actions/content"; import { selectShowNsfw } from "selectors/settings"; import { diff --git a/ui/js/component/fileListSearch/index.js b/ui/js/component/fileListSearch/index.js index 8008aaaef..bdb88faab 100644 --- a/ui/js/component/fileListSearch/index.js +++ b/ui/js/component/fileListSearch/index.js @@ -6,7 +6,7 @@ import { selectCurrentSearchResults, selectSearchQuery, } from "selectors/search"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import FileListSearch from "./view"; const select = state => ({ diff --git a/ui/js/component/fileTile/index.js b/ui/js/component/fileTile/index.js index 0631ae849..2df504226 100644 --- a/ui/js/component/fileTile/index.js +++ b/ui/js/component/fileTile/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doResolveUri } from "actions/content"; import { makeSelectClaimForUri, diff --git a/ui/js/component/header/index.js b/ui/js/component/header/index.js index 464a471c8..857a04beb 100644 --- a/ui/js/component/header/index.js +++ b/ui/js/component/header/index.js @@ -1,9 +1,16 @@ import React from "react"; import { formatCredits } from "util/formatCredits"; import { connect } from "react-redux"; -import { selectIsBackDisabled, selectIsForwardDisabled } from "selectors/app"; +import { + selectIsBackDisabled, + selectIsForwardDisabled, +} from "selectors/navigation"; import { selectBalance } from "selectors/wallet"; -import { doNavigate, doHistoryBack, doHistoryForward } from "actions/app"; +import { + doNavigate, + doHistoryBack, + doHistoryForward, +} from "actions/navigation"; import Header from "./view"; const select = state => ({ diff --git a/ui/js/component/nsfwOverlay/index.js b/ui/js/component/nsfwOverlay/index.js index eb3242410..8395df0ac 100644 --- a/ui/js/component/nsfwOverlay/index.js +++ b/ui/js/component/nsfwOverlay/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import NsfwOverlay from "./view"; const perform = dispatch => ({ diff --git a/ui/js/component/rewardLink/index.js b/ui/js/component/rewardLink/index.js index 3375fbfdf..aa2034835 100644 --- a/ui/js/component/rewardLink/index.js +++ b/ui/js/component/rewardLink/index.js @@ -5,7 +5,7 @@ import { makeSelectRewardByType, makeSelectIsRewardClaimPending, } from "selectors/rewards"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doClaimRewardType, doClaimRewardClearError } from "actions/rewards"; import RewardLink from "./view"; diff --git a/ui/js/component/router/index.js b/ui/js/component/router/index.js index bc40986f5..a5c0e9e63 100644 --- a/ui/js/component/router/index.js +++ b/ui/js/component/router/index.js @@ -1,7 +1,10 @@ import React from "react"; import { connect } from "react-redux"; import Router from "./view.jsx"; -import { selectCurrentPage, selectCurrentParams } from "selectors/app.js"; +import { + selectCurrentPage, + selectCurrentParams, +} from "selectors/navigation.js"; const select = state => ({ params: selectCurrentParams(state), diff --git a/ui/js/component/snackBar/index.js b/ui/js/component/snackBar/index.js index 7996009c4..ff083c3d3 100644 --- a/ui/js/component/snackBar/index.js +++ b/ui/js/component/snackBar/index.js @@ -1,11 +1,10 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate, doRemoveSnackBarSnack } from "actions/app"; +import { doRemoveSnackBarSnack } from "actions/app"; import { selectSnackBarSnacks } from "selectors/app"; import SnackBar from "./view"; const perform = dispatch => ({ - navigate: path => dispatch(doNavigate(path)), removeSnack: () => dispatch(doRemoveSnackBarSnack()), }); diff --git a/ui/js/component/snackBar/view.jsx b/ui/js/component/snackBar/view.jsx index 363c412d6..f2ee2c49d 100644 --- a/ui/js/component/snackBar/view.jsx +++ b/ui/js/component/snackBar/view.jsx @@ -10,7 +10,7 @@ class SnackBar extends React.PureComponent { } render() { - const { navigate, snacks, removeSnack } = this.props; + const { snacks, removeSnack } = this.props; if (!snacks.length) { this._hideTimeout = null; //should be unmounting anyway, but be safe? @@ -33,7 +33,7 @@ class SnackBar extends React.PureComponent { {linkText && linkTarget && navigate(linkTarget)} + navigate={linkTarget} className="snack-bar__action" label={linkText} />} diff --git a/ui/js/component/subHeader/index.js b/ui/js/component/subHeader/index.js index 97a991436..2806828ae 100644 --- a/ui/js/component/subHeader/index.js +++ b/ui/js/component/subHeader/index.js @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; -import { selectCurrentPage, selectHeaderLinks } from "selectors/app"; -import { doNavigate } from "actions/app"; +import { selectCurrentPage, selectHeaderLinks } from "selectors/navigation"; +import { doNavigate } from "actions/navigation"; import SubHeader from "./view"; const select = (state, props) => ({ diff --git a/ui/js/component/userVerify/index.js b/ui/js/component/userVerify/index.js index 043b52c75..b90719084 100644 --- a/ui/js/component/userVerify/index.js +++ b/ui/js/component/userVerify/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doUserIdentityVerify } from "actions/user"; import rewards from "rewards"; import { makeSelectRewardByType } from "selectors/rewards"; diff --git a/ui/js/component/video/index.js b/ui/js/component/video/index.js index dc4b465cd..f28fb94b8 100644 --- a/ui/js/component/video/index.js +++ b/ui/js/component/video/index.js @@ -1,7 +1,7 @@ import React from "react"; import { connect } from "react-redux"; import { doCloseModal } from "actions/app"; -import { doNavigate, doChangeVolume } from "actions/app"; +import { doChangeVolume } from "actions/app"; import { selectCurrentModal, selectVolume } from "selectors/app"; import { doPurchaseUri, doLoadVideo } from "actions/content"; import { diff --git a/ui/js/component/wunderbar/index.js b/ui/js/component/wunderbar/index.js index 9d23f4d26..bd327fd92 100644 --- a/ui/js/component/wunderbar/index.js +++ b/ui/js/component/wunderbar/index.js @@ -2,7 +2,7 @@ import React from "react"; import { connect } from "react-redux"; import lbryuri from "lbryuri.js"; import { selectWunderBarAddress, selectWunderBarIcon } from "selectors/search"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import Wunderbar from "./view"; const select = state => ({ diff --git a/ui/js/constants/action_types.js b/ui/js/constants/action_types.js index edb60621f..84c8e0e06 100644 --- a/ui/js/constants/action_types.js +++ b/ui/js/constants/action_types.js @@ -1,16 +1,19 @@ -export const CHANGE_PATH = "CHANGE_PATH"; export const OPEN_MODAL = "OPEN_MODAL"; export const CLOSE_MODAL = "CLOSE_MODAL"; -export const HISTORY_NAVIGATE = "HISTORY_NAVIGATE"; export const SHOW_SNACKBAR = "SHOW_SNACKBAR"; export const REMOVE_SNACKBAR_SNACK = "REMOVE_SNACKBAR_SNACK"; export const WINDOW_FOCUSED = "WINDOW_FOCUSED"; -export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH"; export const DAEMON_READY = "DAEMON_READY"; export const DAEMON_VERSION_MATCH = "DAEMON_VERSION_MATCH"; export const DAEMON_VERSION_MISMATCH = "DAEMON_VERSION_MISMATCH"; export const VOLUME_CHANGED = "VOLUME_CHANGED"; +// Navigation +export const CHANGE_PATH = "CHANGE_PATH"; +export const CHANGE_AFTER_AUTH_PATH = "CHANGE_AFTER_AUTH_PATH"; +export const WINDOW_SCROLLED = "WINDOW_SCROLLED"; +export const HISTORY_NAVIGATE = "HISTORY_NAVIGATE"; + // Upgrades export const UPGRADE_CANCELLED = "UPGRADE_CANCELLED"; export const DOWNLOAD_UPGRADE = "DOWNLOAD_UPGRADE"; diff --git a/ui/js/main.js b/ui/js/main.js index 42c40a3a2..7dadc22b8 100644 --- a/ui/js/main.js +++ b/ui/js/main.js @@ -6,9 +6,9 @@ import SnackBar from "component/snackBar"; import { Provider } from "react-redux"; import store from "store.js"; import SplashScreen from "component/splash"; -import { doChangePath, doNavigate, doDaemonReady } from "actions/app"; +import { doDaemonReady } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doDownloadLanguages } from "actions/settings"; -import { toQueryString } from "util/query_params"; import * as types from "constants/action_types"; const env = ENV; @@ -28,23 +28,6 @@ window.addEventListener("contextmenu", event => { event.preventDefault(); }); -window.addEventListener("popstate", (event, param) => { - event.preventDefault(); - - const hash = document.location.hash; - let action; - - if (hash !== "") { - const url = hash.replace(/^#/, ""); - const { params, scrollY } = event.state || {}; - const queryString = toQueryString(params); - - app.store.dispatch(doChangePath(`${url}?${queryString}`, { scrollY })); - } else { - app.store.dispatch(doChangePath("/discover")); - } -}); - ipcRenderer.on("open-uri-requested", (event, uri) => { if (uri && uri.startsWith("lbry://")) { app.store.dispatch(doNavigate("/show", { uri })); diff --git a/ui/js/modal/modalCreditIntro/index.js b/ui/js/modal/modalCreditIntro/index.js index 37e72dc9a..c793ec22a 100644 --- a/ui/js/modal/modalCreditIntro/index.js +++ b/ui/js/modal/modalCreditIntro/index.js @@ -1,6 +1,7 @@ import React from "react"; import { connect } from "react-redux"; -import { doCloseModal, doAuthNavigate } from "actions/app"; +import { doCloseModal } from "actions/app"; +import { doAuthNavigate } from "actions/navigation"; import { doSetClientSetting } from "actions/settings"; import { selectUserIsRewardApproved } from "selectors/user"; import { selectBalance } from "selectors/wallet"; diff --git a/ui/js/modal/modalInsufficientCredits/index.js b/ui/js/modal/modalInsufficientCredits/index.js index eefaa7c68..c50d34d44 100644 --- a/ui/js/modal/modalInsufficientCredits/index.js +++ b/ui/js/modal/modalInsufficientCredits/index.js @@ -1,6 +1,7 @@ import React from "react"; import { connect } from "react-redux"; -import { doCloseModal, doNavigate } from "actions/app"; +import { doCloseModal } from "actions/app"; +import { doNavigate } from "actions/navigation"; import ModalInsufficientCredits from "./view"; const select = state => ({}); diff --git a/ui/js/modal/modalRemoveFile/index.js b/ui/js/modal/modalRemoveFile/index.js index 211bdff26..f64c1987e 100644 --- a/ui/js/modal/modalRemoveFile/index.js +++ b/ui/js/modal/modalRemoveFile/index.js @@ -1,9 +1,8 @@ import React from "react"; import { connect } from "react-redux"; -import { doCloseModal, doHistoryBack } from "actions/app"; +import { doCloseModal } from "actions/app"; import { doDeleteFileAndGoBack } from "actions/file_info"; import { makeSelectClaimForUriIsMine } from "selectors/claims"; -import batchActions from "util/batchActions"; import ModalRemoveFile from "./view"; diff --git a/ui/js/modal/modalRouter/index.js b/ui/js/modal/modalRouter/index.js index 9bf9601c5..79f02385f 100644 --- a/ui/js/modal/modalRouter/index.js +++ b/ui/js/modal/modalRouter/index.js @@ -1,11 +1,12 @@ import React from "react"; import { connect } from "react-redux"; -import { selectCurrentModal, selectCurrentPage } from "selectors/app"; import { doOpenModal } from "actions/app"; +import * as settings from "constants/settings"; +import { selectCurrentModal } from "selectors/app"; +import { selectCurrentPage } from "selectors/navigation"; +import { selectCostForCurrentPageUri } from "selectors/cost_info"; import { makeSelectClientSetting } from "selectors/settings"; import { selectUser } from "selectors/user"; -import { selectCostForCurrentPageUri } from "selectors/cost_info"; -import * as settings from "constants/settings"; import { selectBalance } from "selectors/wallet"; import ModalRouter from "./view"; diff --git a/ui/js/page/auth/index.js b/ui/js/page/auth/index.js index 541504dbb..205400088 100644 --- a/ui/js/page/auth/index.js +++ b/ui/js/page/auth/index.js @@ -1,7 +1,7 @@ import React from "react"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { connect } from "react-redux"; -import { selectPathAfterAuth } from "selectors/app"; +import { selectPathAfterAuth } from "selectors/navigation"; import { selectAuthenticationIsPending, selectEmailToVerify, diff --git a/ui/js/page/channel/index.js b/ui/js/page/channel/index.js index dfd75d5fe..95134bdc3 100644 --- a/ui/js/page/channel/index.js +++ b/ui/js/page/channel/index.js @@ -9,8 +9,8 @@ import { makeSelectClaimsInChannelForCurrentPage, makeSelectFetchingChannelClaims, } from "selectors/claims"; -import { selectCurrentParams } from "selectors/app"; -import { doNavigate } from "actions/app"; +import { selectCurrentParams } from "selectors/navigation"; +import { doNavigate } from "actions/navigation"; import { makeSelectTotalPagesForChannel } from "selectors/content"; import ChannelPage from "./view"; diff --git a/ui/js/page/file/index.js b/ui/js/page/file/index.js index 26b98c93a..19cdde31f 100644 --- a/ui/js/page/file/index.js +++ b/ui/js/page/file/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doFetchFileInfo } from "actions/file_info"; import { makeSelectFileInfoForUri } from "selectors/file_info"; import { selectRewardContentClaimIds } from "selectors/content"; diff --git a/ui/js/page/fileListDownloaded/index.js b/ui/js/page/fileListDownloaded/index.js index c2473e02a..449d3f196 100644 --- a/ui/js/page/fileListDownloaded/index.js +++ b/ui/js/page/fileListDownloaded/index.js @@ -10,7 +10,7 @@ import { selectIsFetchingClaimListMine, } from "selectors/claims"; import { doFetchClaimListMine } from "actions/content"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doCancelAllResolvingUris } from "actions/content"; import FileListDownloaded from "./view"; diff --git a/ui/js/page/fileListPublished/index.js b/ui/js/page/fileListPublished/index.js index 8100c2977..c6bf2a43a 100644 --- a/ui/js/page/fileListPublished/index.js +++ b/ui/js/page/fileListPublished/index.js @@ -7,7 +7,7 @@ import { selectIsFetchingClaimListMine, } from "selectors/claims"; import { doClaimRewardType } from "actions/rewards"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import { doCancelAllResolvingUris } from "actions/content"; import FileListPublished from "./view"; diff --git a/ui/js/page/help/index.js b/ui/js/page/help/index.js index 93492d915..601cdb8a7 100644 --- a/ui/js/page/help/index.js +++ b/ui/js/page/help/index.js @@ -1,5 +1,5 @@ import React from "react"; -import { doAuthNavigate } from "actions/app"; +import { doAuthNavigate } from "actions/navigation"; import { connect } from "react-redux"; import { doFetchAccessToken } from "actions/user"; import { selectAccessToken, selectUser } from "selectors/user"; diff --git a/ui/js/page/publish/index.js b/ui/js/page/publish/index.js index 1816bf2d5..f0547941d 100644 --- a/ui/js/page/publish/index.js +++ b/ui/js/page/publish/index.js @@ -1,6 +1,6 @@ import React from "react"; import { connect } from "react-redux"; -import { doNavigate, doHistoryBack } from "actions/app"; +import { doNavigate, doHistoryBack } from "actions/navigation"; import { doClaimRewardType } from "actions/rewards"; import { selectMyClaims, diff --git a/ui/js/page/rewards/index.js b/ui/js/page/rewards/index.js index db479ff15..e9dca47f9 100644 --- a/ui/js/page/rewards/index.js +++ b/ui/js/page/rewards/index.js @@ -5,7 +5,7 @@ import { selectUnclaimedRewards, } from "selectors/rewards"; import { selectUser } from "selectors/user"; -import { doAuthNavigate, doNavigate } from "actions/app"; +import { doAuthNavigate, doNavigate } from "actions/navigation"; import { doRewardList } from "actions/rewards"; import RewardsPage from "./view"; diff --git a/ui/js/page/search/index.js b/ui/js/page/search/index.js index c8d93fae1..a6c571882 100644 --- a/ui/js/page/search/index.js +++ b/ui/js/page/search/index.js @@ -5,7 +5,7 @@ import { selectSearchQuery, selectCurrentSearchResults, } from "selectors/search"; -import { doNavigate } from "actions/app"; +import { doNavigate } from "actions/navigation"; import SearchPage from "./view"; const select = state => ({ diff --git a/ui/js/reducers/app.js b/ui/js/reducers/app.js index dd0237f28..857a5a708 100644 --- a/ui/js/reducers/app.js +++ b/ui/js/reducers/app.js @@ -1,12 +1,6 @@ import * as types from "constants/action_types"; import * as modalTypes from "constants/modal_types"; -const currentPath = () => { - const hash = document.location.hash; - if (hash !== "") return hash.replace(/^#/, ""); - else return "/discover"; -}; - const { remote } = require("electron"); const application = remote.app; const win = remote.BrowserWindow.getFocusedWindow(); @@ -14,28 +8,18 @@ const win = remote.BrowserWindow.getFocusedWindow(); const reducers = {}; const defaultState = { isLoaded: false, - isBackDisabled: true, - isForwardDisabled: true, - currentPath: currentPath(), - pathAfterAuth: "/discover", platform: process.platform, upgradeSkipped: sessionStorage.getItem("upgradeSkipped"), daemonVersionMatched: null, daemonReady: false, hasSignature: false, badgeNumber: 0, - history: { index: 0, stack: [] }, volume: sessionStorage.getItem("volume") || 1, }; reducers[types.DAEMON_READY] = function(state, action) { - const { history } = state; - const { page } = action.data; - history.stack.push(page); - return Object.assign({}, state, { daemonReady: true, - history, }); }; @@ -52,18 +36,6 @@ reducers[types.DAEMON_VERSION_MISMATCH] = function(state, action) { }); }; -reducers[types.CHANGE_PATH] = function(state, action) { - return Object.assign({}, state, { - currentPath: action.data.path, - }); -}; - -reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) { - return Object.assign({}, state, { - pathAfterAuth: action.data.path, - }); -}; - reducers[types.UPGRADE_CANCELLED] = function(state, action) { return Object.assign({}, state, { downloadProgress: null, @@ -171,55 +143,6 @@ reducers[types.WINDOW_FOCUSED] = function(state, action) { }); }; -reducers[types.HISTORY_NAVIGATE] = (state, action) => { - let page = false; - let location = false; - - // Get history from state - const { history } = state; - - if (action.data.page) { - // Get page - page = action.data.page; - } else if (action.data.location) { - // Get new location - location = action.data.location; - } - - // Add new location to stack - if (location) { - const lastItem = history.stack.length - 1; - - // Check for duplicated - let is_duplicate = lastItem > -1 - ? history.stack[lastItem].location === location - : false; - - if (!is_duplicate) { - // Create new page - page = { - index: history.stack.length, - location, - }; - - // Update index - history.index = history.stack.length; - - // Add to stack - history.stack.push(page); - } - } else if (page) { - // Update index - history.index = page.index; - } - - return Object.assign({}, state, { - history, - isBackDisabled: history.index === 0, // First page - isForwardDisabled: history.index === history.stack.length - 1, // Last page - }); -}; - reducers[types.VOLUME_CHANGED] = function(state, action) { return Object.assign({}, state, { volume: action.data.volume, diff --git a/ui/js/reducers/navigation.js b/ui/js/reducers/navigation.js new file mode 100644 index 000000000..eca9bd008 --- /dev/null +++ b/ui/js/reducers/navigation.js @@ -0,0 +1,85 @@ +import * as types from "constants/action_types"; +import { parseQueryParams } from "util/query_params"; + +const currentPath = () => { + const hash = document.location.hash; + if (hash !== "") return hash.replace(/^#/, ""); + else return "/discover"; +}; + +const reducers = {}; +const defaultState = { + currentPath: currentPath(), + pathAfterAuth: "/discover", + index: 0, + stack: [], +}; + +reducers[types.DAEMON_READY] = function(state, action) { + const { currentPath } = state; + const params = parseQueryParams(currentPath.split("?")[1] || ""); + + return Object.assign({}, state, { + stack: [{ path: currentPath, scrollY: 0 }], + }); +}; + +reducers[types.CHANGE_PATH] = function(state, action) { + return Object.assign({}, state, { + currentPath: action.data.path, + }); +}; + +reducers[types.CHANGE_AFTER_AUTH_PATH] = function(state, action) { + return Object.assign({}, state, { + pathAfterAuth: action.data.path, + }); +}; + +reducers[types.HISTORY_NAVIGATE] = (state, action) => { + const { stack, index } = state; + + let newState = {}; + + const path = action.data.url, + previousIndex = index - 1; + + // Check for duplicated + if (action.data.index >= 0) { + newState.index = action.data.index; + } else if ( + previousIndex === -1 || + !stack[previousIndex] || + stack[previousIndex].path !== path + ) { + newState.stack = [...stack.slice(0, index + 1), { path, scrollY: 0 }]; + newState.index = newState.stack.length - 1; + } + + history.replaceState(null, null, "#" + path); //this allows currentPath() to retain the URL on reload + + return Object.assign({}, state, newState); +}; + +reducers[types.WINDOW_SCROLLED] = (state, action) => { + const { stack, index } = state; + const { scrollY } = action.data; + + return Object.assign({}, state, { + stack: state.stack.map((stackItem, itemIndex) => { + if (itemIndex !== index) { + return stackItem; + } + return { + ...stackItem, + scrollY: scrollY, + }; + }), + }); +}; + +export default function reducer(state = defaultState, action) { + const handler = reducers[action.type]; + if (handler) return handler(state, action); + return state; +} diff --git a/ui/js/selectors/app.js b/ui/js/selectors/app.js index eb950f3b1..3e8e68223 100644 --- a/ui/js/selectors/app.js +++ b/ui/js/selectors/app.js @@ -1,88 +1,7 @@ import { createSelector } from "reselect"; -import { parseQueryParams, toQueryString } from "util/query_params"; -import * as settings from "constants/settings.js"; -import lbryuri from "lbryuri"; export const _selectState = state => state.app || {}; -export const selectIsLoaded = createSelector( - _selectState, - state => state.isLoaded -); - -export const selectCurrentPath = createSelector( - _selectState, - state => state.currentPath -); - -export const selectCurrentPage = createSelector(selectCurrentPath, path => { - return path.replace(/^\//, "").split("?")[0]; -}); - -export const selectCurrentParams = createSelector(selectCurrentPath, path => { - if (path === undefined) return {}; - if (!path.match(/\?/)) return {}; - - return parseQueryParams(path.split("?")[1]); -}); - -export const selectPageTitle = createSelector( - selectCurrentPage, - selectCurrentParams, - (page, params) => { - switch (page) { - case "settings": - return __("Settings"); - case "report": - return __("Report"); - case "wallet": - return __("Wallet"); - case "send": - return __("Send Credits"); - case "receive": - return __("Wallet Address"); - case "backup": - return __("Backup Your Wallet"); - case "rewards": - return __("Rewards"); - case "invite": - return __("Invites"); - case "start": - return __("Start"); - case "publish": - return __("Publish"); - case "help": - return __("Help"); - case "developer": - return __("Developer"); - case "search": - return params.query - ? __("Search results for %s", params.query) - : __("Search"); - case "show": { - const parts = [lbryuri.normalize(params.uri)]; - // If the params has any keys other than "uri" - if (Object.keys(params).length > 1) { - parts.push(toQueryString(Object.assign({}, params, { uri: null }))); - } - return parts.join("?"); - } - case "downloaded": - return __("Downloads & Purchases"); - case "published": - return __("Publishes"); - case "discover": - return __("Home"); - case false: - case null: - case "": - return ""; - default: - return page[0].toUpperCase() + (page.length > 0 ? page.substr(1) : ""); - } - } -); - export const selectPlatform = createSelector( _selectState, state => state.platform @@ -137,41 +56,6 @@ export const selectDownloadComplete = createSelector( state => state.upgradeDownloadCompleted ); -export const selectHeaderLinks = createSelector(selectCurrentPage, page => { - // This contains intentional fall throughs - switch (page) { - case "wallet": - case "history": - case "send": - case "receive": - case "invite": - case "rewards": - case "backup": - return { - wallet: __("Overview"), - history: __("History"), - send: __("Send"), - receive: __("Receive"), - rewards: __("Rewards"), - invite: __("Invites"), - }; - case "downloaded": - case "published": - return { - downloaded: __("Downloaded"), - published: __("Published"), - }; - case "settings": - case "help": - return { - settings: __("Settings"), - help: __("Help"), - }; - default: - return null; - } -}); - export const selectUpgradeSkipped = createSelector( _selectState, state => state.upgradeSkipped @@ -222,41 +106,4 @@ export const selectCurrentLanguage = createSelector( () => app.i18n.getLocale() || "en" ); -export const selectPathAfterAuth = createSelector( - _selectState, - state => state.pathAfterAuth -); - -export const selectIsBackDisabled = createSelector( - _selectState, - state => state.isBackDisabled -); - -export const selectIsForwardDisabled = createSelector( - _selectState, - state => state.isForwardDisabled -); - -export const selectHistoryBack = createSelector(_selectState, state => { - const { history } = state; - const index = history.index - 1; - - // Check if page exists - if (index > -1) { - // Get back history - return history.stack[index]; - } -}); - -export const selectHistoryForward = createSelector(_selectState, state => { - const { history } = state; - const index = history.index + 1; - - // Check if page exists - if (index <= history.stack.length) { - // Get forward history - return history.stack[index]; - } -}); - export const selectVolume = createSelector(_selectState, state => state.volume); diff --git a/ui/js/selectors/claims.js b/ui/js/selectors/claims.js index 2c4b4b511..630e72c3e 100644 --- a/ui/js/selectors/claims.js +++ b/ui/js/selectors/claims.js @@ -1,5 +1,5 @@ import { createSelector } from "reselect"; -import { selectCurrentParams } from "selectors/app"; +import { selectCurrentParams } from "selectors/navigation"; import lbryuri from "lbryuri"; const _selectState = state => state.claims || {}; diff --git a/ui/js/selectors/cost_info.js b/ui/js/selectors/cost_info.js index 034c4572f..b40d62d5e 100644 --- a/ui/js/selectors/cost_info.js +++ b/ui/js/selectors/cost_info.js @@ -1,5 +1,5 @@ import { createSelector } from "reselect"; -import { selectCurrentParams } from "./app"; +import { selectCurrentParams } from "selectors/navigation"; export const _selectState = state => state.costInfo || {}; diff --git a/ui/js/selectors/navigation.js b/ui/js/selectors/navigation.js new file mode 100644 index 000000000..16e1c53f9 --- /dev/null +++ b/ui/js/selectors/navigation.js @@ -0,0 +1,129 @@ +import { createSelector } from "reselect"; +import { parseQueryParams, toQueryString } from "util/query_params"; +import * as settings from "constants/settings.js"; +import lbryuri from "lbryuri"; + +export const _selectState = state => state.navigation || {}; + +export const selectCurrentPath = createSelector( + _selectState, + state => state.currentPath +); + +export const selectCurrentPage = createSelector(selectCurrentPath, path => { + return path.replace(/^\//, "").split("?")[0]; +}); + +export const selectCurrentParams = createSelector(selectCurrentPath, path => { + if (path === undefined) return {}; + if (!path.match(/\?/)) return {}; + + return parseQueryParams(path.split("?")[1]); +}); + +export const selectHeaderLinks = createSelector(selectCurrentPage, page => { + // This contains intentional fall throughs + switch (page) { + case "wallet": + case "send": + case "receive": + case "rewards": + case "backup": + return { + wallet: __("Overview"), + send: __("Send"), + receive: __("Receive"), + rewards: __("Rewards"), + }; + case "downloaded": + case "published": + return { + downloaded: __("Downloaded"), + published: __("Published"), + }; + case "settings": + case "help": + return { + settings: __("Settings"), + help: __("Help"), + }; + default: + return null; + } +}); + +export const selectPageTitle = createSelector( + selectCurrentPage, + selectCurrentParams, + (page, params) => { + switch (page) { + case "settings": + return __("Settings"); + case "report": + return __("Report"); + case "wallet": + return __("Wallet"); + case "send": + return __("Send"); + case "receive": + return __("Receive"); + case "backup": + return __("Backup"); + case "rewards": + return __("Rewards"); + case "start": + return __("Start"); + case "publish": + return __("Publish"); + case "help": + return __("Help"); + case "developer": + return __("Developer"); + case "search": + return params.query + ? __("Search results for %s", params.query) + : __("Search"); + case "show": { + const parts = [lbryuri.normalize(params.uri)]; + // If the params has any keys other than "uri" + if (Object.keys(params).length > 1) { + parts.push(toQueryString(Object.assign({}, params, { uri: null }))); + } + return parts.join("?"); + } + case "downloaded": + return __("Downloads & Purchases"); + case "published": + return __("Publishes"); + case "discover": + return __("Home"); + default: + return ""; + } + } +); + +export const selectPathAfterAuth = createSelector( + _selectState, + state => state.pathAfterAuth +); + +export const selectIsBackDisabled = createSelector( + _selectState, + state => state.index === 0 +); + +export const selectIsForwardDisabled = createSelector( + _selectState, + state => state.index === state.stack.length - 1 +); + +export const selectHistoryIndex = createSelector( + _selectState, + state => state.index +); + +export const selectHistoryStack = createSelector( + _selectState, + state => state.stack +); diff --git a/ui/js/selectors/search.js b/ui/js/selectors/search.js index 2ef8a1410..457126597 100644 --- a/ui/js/selectors/search.js +++ b/ui/js/selectors/search.js @@ -1,5 +1,5 @@ import { createSelector } from "reselect"; -import { selectPageTitle, selectCurrentPage } from "selectors/app"; +import { selectPageTitle, selectCurrentPage } from "selectors/navigation"; export const _selectState = state => state.search || {}; diff --git a/ui/js/selectors/wallet.js b/ui/js/selectors/wallet.js index 1334a17db..84ca37f8e 100644 --- a/ui/js/selectors/wallet.js +++ b/ui/js/selectors/wallet.js @@ -1,5 +1,4 @@ import { createSelector } from "reselect"; -import { selectCurrentPage, selectDaemonReady } from "selectors/app"; export const _selectState = state => state.wallet || {}; diff --git a/ui/js/store.js b/ui/js/store.js index 5eb84d4cf..af1e68c3a 100644 --- a/ui/js/store.js +++ b/ui/js/store.js @@ -5,6 +5,7 @@ import claimsReducer from "reducers/claims"; import contentReducer from "reducers/content"; import costInfoReducer from "reducers/cost_info"; import fileInfoReducer from "reducers/file_info"; +import navigationReducer from "reducers/navigation"; import rewardsReducer from "reducers/rewards"; import searchReducer from "reducers/search"; import settingsReducer from "reducers/settings"; @@ -13,8 +14,8 @@ import walletReducer from "reducers/wallet"; import { persistStore, autoRehydrate } from "redux-persist"; import createCompressor from "redux-persist-transform-compress"; import createFilter from "redux-persist-transform-filter"; -import { REHYDRATE } from "redux-persist/constants"; -import createActionBuffer from "redux-action-buffer"; +//import { REHYDRATE } from "redux-persist/constants"; +//import createActionBuffer from "redux-action-buffer"; const localForage = require("localforage"); const redux = require("redux"); @@ -55,6 +56,7 @@ function enableBatching(reducer) { const reducers = redux.combineReducers({ app: appReducer, + navigation: navigationReducer, availability: availabilityReducer, claims: claimsReducer, fileInfo: fileInfoReducer,