Skip to content

Commit

Permalink
client: Reload entries by clicking on active menu item
Browse files Browse the repository at this point in the history
This is quite hacky – router does not re-render the route when clicking
on an already active link because its data is based on the document.location.
To bypass this, we need to pass an extra value in the location’s state object
that will let us know we should reload. And for similar reasons, it cannot
just be a boolean (or we would have to clear the state each time to be able
to tell when the value changes again) so we are back at increasing counters.

Unfortunately, that is not end of it since any navigation would clear
the state, triggering another reload so we also need to use another
counter that only increases when the counter in location state increases.

Fixes: #1287
  • Loading branch information
jtojnar committed Jan 5, 2022
1 parent 9407240 commit d61ee79
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 27 deletions.
30 changes: 30 additions & 0 deletions assets/js/helpers/hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useState } from 'react';
import { useLocation } from 'react-router-dom';

/**
* Changes its return value whenever the value of forceReload field
* in the location state increases.
*/
export function useShouldReload() {
const location = useLocation();
const forceReload = location?.state?.forceReload;
const [oldForceReload, setOldForceReload] = useState(forceReload);

if (oldForceReload !== forceReload) {
setOldForceReload(forceReload);
}

// The location state is not persisted during navigation
// so forceReload would change to undefined on successive navigation,
// triggering an unwanter reload.
// We use a separate counter to prevent that.
const [reloadCounter, setReloadCounter] = useState(0);
if (forceReload !== undefined && forceReload !== oldForceReload) {
let newReloadCounter = reloadCounter + 1;

setReloadCounter(newReloadCounter);
return newReloadCounter;
}

return reloadCounter;
}
9 changes: 9 additions & 0 deletions assets/js/helpers/uri.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,12 @@ export function makeEntriesLink(location, { filter, category, id, search }) {

return path + (searchParam ? `?search=${encodeURIComponent(searchParam)}` : '');
}

export function forceReload(location) {
const state = location.state ?? {};

return {
...state,
forceReload: (state.forceReload ?? 0) + 1,
};
}
21 changes: 11 additions & 10 deletions assets/js/templates/EntriesPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { LoadingState } from '../requests/LoadingState';
import { Spinner, SpinnerBig } from './Spinner';
import classNames from 'classnames';
import { LocalizationContext } from '../helpers/i18n';
import { useShouldReload } from '../helpers/hooks';
import { forceReload } from '../helpers/uri';
import { HttpError } from '../errors';

function reloadList({ fetchParams, abortController, append = false, waitForSync = true, entryId = null, setLoadingState }) {
Expand Down Expand Up @@ -138,10 +140,11 @@ function handleRefreshSource({ event, source, setLoadingState, setNavExpanded, r
});
}

export function EntriesPage({ entries, hasMore, loadingState, setLoadingState, forceReload, selectedEntry, expandedEntries, setNavExpanded, navSourcesExpanded, reload }) {
export function EntriesPage({ entries, hasMore, loadingState, setLoadingState, selectedEntry, expandedEntries, setNavExpanded, navSourcesExpanded, reload }) {
const allowedToUpdate = !selfoss.config.authEnabled || selfoss.config.allowPublicUpdate || selfoss.loggedin.value;

const location = useLocation();
const forceReload = useShouldReload();
const searchText = React.useMemo(() => {
const queryString = new URLSearchParams(location.search);

Expand Down Expand Up @@ -358,7 +361,6 @@ EntriesPage.propTypes = {
hasMore: PropTypes.bool.isRequired,
loadingState: PropTypes.oneOf(Object.values(LoadingState)).isRequired,
setLoadingState: PropTypes.func.isRequired,
forceReload: PropTypes.number.isRequired,
selectedEntry: nullable(PropTypes.number).isRequired,
expandedEntries: PropTypes.objectOf(PropTypes.bool).isRequired,
setNavExpanded: PropTypes.func.isRequired,
Expand All @@ -377,10 +379,6 @@ const initialState = {
selectedEntry: null,
expandedEntries: {},
loadingState: LoadingState.INITIAL,
/**
* HACK: A counter that is increased every time reload action (r key) is triggered.
*/
forceReload: 0,
};

export default class StateHolder extends React.Component {
Expand Down Expand Up @@ -799,9 +797,12 @@ export default class StateHolder extends React.Component {
}

reload() {
this.setState({
...initialState,
forceReload: this.state.forceReload + 1,
/**
* HACK: A counter that is increased every time reload action (r key) is triggered.
*/
selfoss.history.replace({
...this.props.location,
state: forceReload(this.props.location),
});
}

Expand All @@ -814,7 +815,6 @@ export default class StateHolder extends React.Component {
hasMore={this.state.hasMore}
loadingState={this.state.loadingState}
setLoadingState={this.setLoadingState}
forceReload={this.state.forceReload}
setNavExpanded={this.props.setNavExpanded}
navSourcesExpanded={this.props.navSourcesExpanded}
reload={this.reload}
Expand All @@ -824,6 +824,7 @@ export default class StateHolder extends React.Component {
}

StateHolder.propTypes = {
location: PropTypes.object.isRequired,
match: PropTypes.object.isRequired,
setNavExpanded: PropTypes.func.isRequired,
navSourcesExpanded: PropTypes.bool.isRequired,
Expand Down
38 changes: 32 additions & 6 deletions assets/js/templates/NavFilters.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link, useLocation, useRouteMatch } from 'react-router-dom';
import { Link, useRouteMatch } from 'react-router-dom';
import classNames from 'classnames';
import { FilterType } from '../Filter';
import { makeEntriesLink, ENTRIES_ROUTE_PATTERN } from '../helpers/uri';
import { forceReload, makeEntriesLink, ENTRIES_ROUTE_PATTERN } from '../helpers/uri';
import Collapse from '@kunukn/react-collapse';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import * as icons from '../icons';
Expand All @@ -21,7 +21,6 @@ export default function NavFilters({
}) {
const [expanded, setExpanded] = React.useState(true);

const location = useLocation();
// useParams does not seem to work.
const match = useRouteMatch(ENTRIES_ROUTE_PATTERN);
const params = match !== null ? match.params : {};
Expand All @@ -36,6 +35,33 @@ export default function NavFilters({
[setNavExpanded]
);

const newestLink = React.useCallback(
(location) => ({
...location,
pathname: makeEntriesLink(location, { filter: FilterType.NEWEST, id: null }),
state: forceReload(location),
}),
[]
);

const unreadLink = React.useCallback(
(location) => ({
...location,
pathname: makeEntriesLink(location, { filter: FilterType.UNREAD, id: null }),
state: forceReload(location),
}),
[]
);

const starredLink = React.useCallback(
(location) => ({
...location,
pathname: makeEntriesLink(location, { filter: FilterType.STARRED, id: null }),
state: forceReload(location),
}),
[]
);

const _ = React.useContext(LocalizationContext);

return (
Expand All @@ -44,14 +70,14 @@ export default function NavFilters({
<Collapse isOpen={expanded} className="collapse-css-transition">
<ul id="nav-filter" aria-labelledby="nav-filter-title">
<li>
<Link id="nav-filter-newest" to={makeEntriesLink(location, { filter: FilterType.NEWEST, id: null })} className={classNames({'nav-filter-newest': true, active: params.filter === FilterType.NEWEST})} onClick={collapseNav}>
<Link id="nav-filter-newest" to={newestLink} className={classNames({'nav-filter-newest': true, active: params.filter === FilterType.NEWEST})} onClick={collapseNav}>
{_('newest')}
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: allItemsCount !== allItemsOfflineCount && allItemsOfflineCount})} title={_('offline_count')}>{allItemsOfflineCount > 0 ? allItemsOfflineCount : ''}</span>
<span className="count" title={_('online_count')}>{allItemsCount > 0 ? allItemsCount : ''}</span>
</Link>
</li>
<li>
<Link id="nav-filter-unread" to={makeEntriesLink(location, { filter: FilterType.UNREAD, id: null })} className={classNames({'nav-filter-unread': true, active: params.filter === FilterType.UNREAD})} onClick={collapseNav}>
<Link id="nav-filter-unread" to={unreadLink} className={classNames({'nav-filter-unread': true, active: params.filter === FilterType.UNREAD})} onClick={collapseNav}>
{_('unread')}
<span className={classNames({'unread-count': true, offline: offlineState, online: !offlineState, unread: unreadItemsCount > 0})}>
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: unreadItemsCount !== unreadItemsOfflineCount && unreadItemsOfflineCount})} title={_('offline_count')}>{unreadItemsOfflineCount > 0 ? unreadItemsOfflineCount : ''}</span>
Expand All @@ -60,7 +86,7 @@ export default function NavFilters({
</Link>
</li>
<li>
<Link id="nav-filter-starred" to={makeEntriesLink(location, { filter: FilterType.STARRED, id: null })} className={classNames({'nav-filter-starred': true, active: params.filter === FilterType.STARRED})} onClick={collapseNav}>
<Link id="nav-filter-starred" to={starredLink} className={classNames({'nav-filter-starred': true, active: params.filter === FilterType.STARRED})} onClick={collapseNav}>
{_('starred')}
<span className={classNames({'offline-count': true, offline: offlineState, online: !offlineState, diff: starredItemsCount !== starredItemsOfflineCount && starredItemsOfflineCount})} title={_('offline_count')}>{starredItemsOfflineCount > 0 ? starredItemsOfflineCount : ''}</span>
<span className="count" title={_('online_count')}>{starredItemsCount > 0 ? starredItemsCount : ''}</span>
Expand Down
15 changes: 11 additions & 4 deletions assets/js/templates/NavSources.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link, useLocation, useRouteMatch } from 'react-router-dom';
import { Link, useRouteMatch } from 'react-router-dom';
import { usePreviousImmediate } from 'rooks';
import classNames from 'classnames';
import { unescape } from 'html-escaper';
import { makeEntriesLink, ENTRIES_ROUTE_PATTERN } from '../helpers/uri';
import { forceReload, makeEntriesLink, ENTRIES_ROUTE_PATTERN } from '../helpers/uri';
import Collapse from '@kunukn/react-collapse';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { LoadingState } from '../requests/LoadingState';
Expand Down Expand Up @@ -33,13 +33,20 @@ function handleTitleClick({ setExpanded, sourcesState, setSourcesState, setSourc
}

function Source({ source, active, collapseNav }) {
const location = useLocation();
const link = React.useCallback(
(location) => ({
...location,
pathname: makeEntriesLink(location, { category: `source-${source.id}`, id: null }),
state: forceReload(location),
}),
[source.id]
);

return (
<li
className={classNames({ read: source.unread === 0 })}
>
<Link to={makeEntriesLink(location, { category: `source-${source.id}`, id: null })} className={classNames({active, unread: source.unread > 0})} onClick={collapseNav}>
<Link to={link} className={classNames({active, unread: source.unread > 0})} onClick={collapseNav}>
<span className="nav-source">{unescape(source.title)}</span>
<span className="unread">{source.unread > 0 ? source.unread : ''}</span>
</Link>
Expand Down
23 changes: 16 additions & 7 deletions assets/js/templates/NavTags.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import nullable from 'prop-types-nullable';
import { Link, useLocation, useRouteMatch } from 'react-router-dom';
import { Link, useRouteMatch } from 'react-router-dom';
import classNames from 'classnames';
import { unescape } from 'html-escaper';
import { makeEntriesLink, ENTRIES_ROUTE_PATTERN } from '../helpers/uri';
import { forceReload, makeEntriesLink, ENTRIES_ROUTE_PATTERN } from '../helpers/uri';
import ColorChooser from './ColorChooser';
import { updateTag } from '../requests/tags';
import Collapse from '@kunukn/react-collapse';
Expand All @@ -13,7 +13,6 @@ import * as icons from '../icons';
import { LocalizationContext } from '../helpers/i18n';

function Tag({ tag, active, collapseNav }) {
const location = useLocation();
const _ = React.useContext(LocalizationContext);
const tagName = tag !== null ? tag.tag : null;

Expand All @@ -31,15 +30,25 @@ function Tag({ tag, active, collapseNav }) {
[tagName]
);

const category = tag === null ? 'all' : `tag-${tag.tag}`;
const link = React.useCallback(
(location) => ({
...location,
pathname: makeEntriesLink(location, {
category,
id: null
}),
state: forceReload(location),
}),
[category]
);

return (
<li
className={classNames({ read: tag !== null && tag.unread === 0 })}
>
<Link
to={makeEntriesLink(location, {
category: tag === null ? 'all' : `tag-${tag.tag}`,
id: null
})}
to={link}
className={classNames({ 'nav-tags-all': tag === null, active })}
onClick={collapseNav}
>
Expand Down

0 comments on commit d61ee79

Please sign in to comment.