Add pagination to channel pages #354

Merged
6ea86b96 merged 2 commits from infinite-scroll into master 2017-07-29 21:35:45 +02:00
19 changed files with 316 additions and 65 deletions

View file

@ -8,12 +8,20 @@ Web UI version numbers should always match the corresponding version of LBRY App
## [Unreleased]
### Added
*
*
* Identity verification for new reward participants
* Support rich markup in publishing descriptions and show pages.
* Release past publishing claims (and recover LBC) via the UI
* Added transition to card hovers to smooth animation
* Use randomly colored tiles when image is missing from metadata
* Added a loading message to file actions
* URL is auto suggested in Publish Page
* Added infinite scroll to channel pages
### Changed
*
*
* Publishing revamped. Editing claims is much easier.
* Publish page now use `claim_list` rather than `file_list`
* Daemon updated to [v0.14.2](https://github.com/lbryio/lbry/releases/tag/v0.14.2)
* Made channel claim storage more efficient
### Fixed
*

View file

@ -22,7 +22,7 @@ const { download } = remote.require("electron-dl");
const fs = remote.require("fs");
const { lbrySettings: config } = require("../../../app/package.json");
export function doNavigate(path, params = {}) {
export function doNavigate(path, params = {}, options = {}) {
return function(dispatch, getState) {
let url = path;
if (params) url = `${url}?${toQueryString(params)}`;

View file

@ -262,6 +262,7 @@ export function doLoadVideo(uri) {
type: types.LOADING_VIDEO_FAILED,
data: { uri },
});
dispatch(doOpenModal("timedOut"));
} else {
dispatch(doDownloadFile(uri, streamInfo));
@ -313,22 +314,28 @@ export function doPurchaseUri(uri, purchaseModalName) {
};
}
export function doFetchClaimsByChannel(uri, page = 1) {
export function doFetchClaimsByChannel(uri, page) {
return function(dispatch, getState) {
dispatch({
type: types.FETCH_CHANNEL_CLAIMS_STARTED,
data: { uri },
data: { uri, page },
});
lbry.claim_list_by_channel({ uri, page }).then(result => {
const claimResult = result[uri],
claims = claimResult ? claimResult.claims_in_channel : [];
claims = claimResult ? claimResult.claims_in_channel : [],
totalPages = claimResult
? claimResult.claims_in_channel_pages
: undefined,
currentPage = claimResult ? claimResult.returned_page : undefined;
dispatch({
type: types.FETCH_CHANNEL_CLAIMS_COMPLETED,
data: {
uri,
claims: claims,
claims,
totalPages,
page: currentPage,
},
});
});

View file

@ -25,23 +25,23 @@ const Router = props => {
const { currentPage, params } = props;
return route(currentPage, {
auth: <AuthPage {...params} />,
channel: <ChannelPage {...params} />,
developer: <DeveloperPage {...params} />,
discover: <DiscoverPage {...params} />,
downloaded: <FileListDownloaded {...params} />,
help: <HelpPage {...params} />,
publish: <PublishPage {...params} />,
published: <FileListPublished {...params} />,
receive: <WalletPage {...params} />,
report: <ReportPage {...params} />,
rewards: <RewardsPage {...params} />,
search: <SearchPage {...params} />,
send: <WalletPage {...params} />,
settings: <SettingsPage {...params} />,
show: <ShowPage {...params} />,
start: <StartPage {...params} />,
wallet: <WalletPage {...params} />,
auth: <AuthPage params={params} />,
channel: <ChannelPage params={params} />,
developer: <DeveloperPage params={params} />,
discover: <DiscoverPage params={params} />,
downloaded: <FileListDownloaded params={params} />,
help: <HelpPage params={params} />,
publish: <PublishPage params={params} />,
published: <FileListPublished params={params} />,
receive: <WalletPage params={params} />,
report: <ReportPage params={params} />,
rewards: <RewardsPage params={params} />,
search: <SearchPage params={params} />,
send: <WalletPage params={params} />,
settings: <SettingsPage params={params} />,
show: <ShowPage params={params} />,
start: <StartPage params={params} />,
wallet: <WalletPage params={params} />,
});
};

View file

@ -12,8 +12,10 @@ const select = state => ({
const perform = dispatch => ({
onSearch: query => dispatch(doNavigate("/search", { query })),
onSubmit: query =>
dispatch(doNavigate("/show", { uri: lbryuri.normalize(query) })),
onSubmit: (query, extraParams) =>
dispatch(
doNavigate("/show", { uri: lbryuri.normalize(query), ...extraParams })
),
});
export default connect(select, perform)(Wunderbar);

View file

@ -1,6 +1,7 @@
import React from "react";
import lbryuri from "lbryuri.js";
import { Icon } from "component/common.js";
import { parseQueryParams } from "util/query_params";
class WunderBar extends React.PureComponent {
static TYPING_TIMEOUT = 800;
@ -120,12 +121,15 @@ class WunderBar extends React.PureComponent {
onKeyPress(event) {
if (event.charCode == 13 && this._input.value) {
let uri = null,
method = "onSubmit";
method = "onSubmit",
extraParams = {};
this._resetOnNextBlur = false;
clearTimeout(this._userTypingTimer);
const value = this._input.value.trim();
const parts = this._input.value.trim().split("?");
const value = parts.shift();
if (parts.length > 0) extraParams = parseQueryParams(parts.join(""));
try {
uri = lbryuri.normalize(value);
@ -136,7 +140,7 @@ class WunderBar extends React.PureComponent {
method = "onSearch";
}
this.props[method](uri);
this.props[method](uri, extraParams);
this._input.blur();
}
}

View file

@ -3,24 +3,34 @@ import { connect } from "react-redux";
import { doFetchClaimsByChannel } from "actions/content";
import {
makeSelectClaimForUri,
makeSelectClaimsInChannelForUri,
makeSelectClaimsInChannelForCurrentPage,
makeSelectFetchingChannelClaims,
} from "selectors/claims";
import { selectCurrentParams } from "selectors/app";
import { doNavigate } from "actions/app";
import { makeSelectTotalPagesForChannel } from "selectors/content";
import ChannelPage from "./view";
const makeSelect = () => {
const selectClaim = makeSelectClaimForUri(),
selectClaimsInChannel = makeSelectClaimsInChannelForUri();
selectClaimsInChannel = makeSelectClaimsInChannelForCurrentPage(),
selectFetchingChannelClaims = makeSelectFetchingChannelClaims(),
selectTotalPagesForChannel = makeSelectTotalPagesForChannel();
const select = (state, props) => ({
claim: selectClaim(state, props),
claimsInChannel: selectClaimsInChannel(state, props),
fetching: selectFetchingChannelClaims(state, props),
totalPages: selectTotalPagesForChannel(state, props),
params: selectCurrentParams(state),
});
return select;
};
const perform = dispatch => ({
fetchClaims: uri => dispatch(doFetchClaimsByChannel(uri)),
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
navigate: (path, params) => dispatch(doNavigate(path, params)),
});
export default connect(makeSelect, perform)(ChannelPage);

View file

@ -2,24 +2,41 @@ import React from "react";
import lbryuri from "lbryuri";
import { BusyMessage } from "component/common";
import FileTile from "component/fileTile";
import Link from "component/link";
import ReactPaginate from "react-paginate";
class ChannelPage extends React.PureComponent {
componentDidMount() {
this.fetchClaims(this.props);
const { uri, params, fetchClaims } = this.props;
fetchClaims(uri, params.page || 1);
}
componentWillReceiveProps(nextProps) {
this.fetchClaims(nextProps);
const { params, fetching, fetchClaims } = this.props;
const nextParams = nextProps.params;
if (fetching !== nextParams.page && params.page !== nextParams.page)
fetchClaims(nextProps.uri, nextParams.page);
}
fetchClaims(props) {
if (props.claimsInChannel === undefined) {
props.fetchClaims(props.uri);
}
changePage(pageNumber) {
const { params, currentPage } = this.props;
const newParams = Object.assign({}, params, { page: pageNumber });
this.props.navigate("/show", newParams);
}
render() {
const { claimsInChannel, claim, uri } = this.props;
const {
fetching,
claimsInChannel,
claim,
uri,
params,
totalPages,
} = this.props;
const { page } = params;
let contentList;
if (claimsInChannel === undefined) {
@ -29,14 +46,17 @@ class ChannelPage extends React.PureComponent {
? claimsInChannel.map(claim =>
<FileTile
key={claim.claim_id}
uri={lbryuri.build({ name: claim.name, claimId: claim.claim_id })}
uri={lbryuri.build({
name: claim.name,
claimId: claim.claim_id,
})}
/>
)
: <span className="empty">{__("No content found.")}</span>;
}
return (
<main className="main--single-column">
<div>
<section className="card">
<div className="card__inner">
<div className="card__title-identity"><h1>{uri}</h1></div>
@ -51,7 +71,24 @@ class ChannelPage extends React.PureComponent {
</section>
<h3 className="card-row__header">{__("Published Content")}</h3>
{contentList}
</main>
<div />
{(!fetching || (claimsInChannel && claimsInChannel.length)) &&
totalPages > 1 &&
<ReactPaginate
pageCount={totalPages}
pageRangeDisplayed={2}
previousLabel=""
nextLabel=""
activeClassName="pagination__item--selected"
pageClassName="pagination__item"
previousClassName="pagination__item pagination__item--previous"
nextClassName="pagination__item pagination__item--next"
marginPagesDisplayed={2}
onPageChange={e => this.changePage(e.selected + 1)}
initialPage={parseInt(page - 1)}
containerClassName="pagination"
/>}
</div>
);
}
}

View file

@ -10,8 +10,8 @@ const makeSelect = () => {
selectIsResolving = makeSelectIsResolvingForUri();
const select = (state, props) => ({
claim: selectClaim(state, props),
isResolvingUri: selectIsResolving(state, props),
claim: selectClaim(state, props.params),
isResolvingUri: selectIsResolving(state, props.params),
});
return select;

View file

@ -6,13 +6,15 @@ import FilePage from "page/filePage";
class ShowPage extends React.PureComponent {
componentWillMount() {
const { isResolvingUri, resolveUri, uri } = this.props;
const { isResolvingUri, resolveUri, params } = this.props;
const { uri } = params;
if (!isResolvingUri) resolveUri(uri);
}
componentWillReceiveProps(nextProps) {
const { isResolvingUri, resolveUri, claim, uri } = nextProps;
const { isResolvingUri, resolveUri, claim, params } = nextProps;
const { uri } = params;
if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri);
@ -20,7 +22,8 @@ class ShowPage extends React.PureComponent {
}
render() {
const { claim, uri, isResolvingUri } = this.props;
const { claim, params, isResolvingUri } = this.props;
const { uri } = params;
let innerContent = "";

View file

@ -101,17 +101,44 @@ reducers[types.FETCH_CHANNEL_LIST_MINE_COMPLETED] = function(state, action) {
});
};
reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
const { uri, claims } = action.data;
reducers[types.FETCH_CHANNEL_CLAIMS_STARTED] = function(state, action) {
const { uri, page } = action.data;
const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims);
const newClaims = Object.assign({}, state.claimsByChannel);
if (claims !== undefined) {
newClaims[uri] = claims;
}
fetchingChannelClaims[uri] = page;
return Object.assign({}, state, {
claimsByChannel: newClaims,
fetchingChannelClaims,
});
};
reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
const { uri, claims, page } = action.data;
const claimsByChannel = Object.assign({}, state.claimsByChannel);
const byChannel = Object.assign({}, claimsByChannel[uri]);
const allClaimIds = new Set(byChannel["all"]);
const currentPageClaimIds = [];
const byId = Object.assign({}, state.byId);
const fetchingChannelClaims = Object.assign({}, state.fetchingChannelClaims);
if (claims !== undefined) {
claims.forEach(claim => {
allClaimIds.add(claim.claim_id);
currentPageClaimIds.push(claim.claim_id);
byId[claim.claim_id] = claim;
});
}
byChannel["all"] = allClaimIds;
byChannel[page] = currentPageClaimIds;
claimsByChannel[uri] = byChannel;
delete fetchingChannelClaims[uri];
return Object.assign({}, state, {
claimsByChannel,
byId,
fetchingChannelClaims,
});
};

View file

@ -47,6 +47,17 @@ reducers[types.RESOLVE_URI_CANCELED] = reducers[
});
};
reducers[types.FETCH_CHANNEL_CLAIMS_COMPLETED] = function(state, action) {
const channelPages = Object.assign({}, state.channelPages);
const { uri, totalPages } = action.data;
channelPages[uri] = totalPages;
return Object.assign({}, state, {
channelPages,
});
};
export default function reducer(state = defaultState, action) {
const handler = reducers[action.type];
if (handler) return handler(state, action);

View file

@ -1,5 +1,5 @@
import { createSelector } from "reselect";
import { parseQueryParams } from "util/query_params";
import { parseQueryParams, toQueryString } from "util/query_params";
import lbry from "lbry";
import lbryuri from "lbryuri";
@ -55,8 +55,14 @@ export const selectPageTitle = createSelector(
return params.query
? __("Search results for %s", params.query)
: __("Search");
case "show":
return lbryuri.normalize(params.uri);
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":

View file

@ -1,4 +1,5 @@
import { createSelector } from "reselect";
import { selectCurrentParams } from "selectors/app";
import lbryuri from "lbryuri";
const _selectState = state => state.claims || {};
@ -60,14 +61,59 @@ export const makeSelectClaimForUriIsMine = () => {
return createSelector(selectClaimForUriIsMine, isMine => isMine);
};
export const selectAllFetchingChannelClaims = createSelector(
_selectState,
state => state.fetchingChannelClaims || {}
);
const selectFetchingChannelClaims = (state, props) => {
const allFetchingChannelClaims = selectAllFetchingChannelClaims(state);
return allFetchingChannelClaims[props.uri];
};
export const makeSelectFetchingChannelClaims = (state, props) => {
return createSelector(selectFetchingChannelClaims, fetching => fetching);
};
export const selectClaimsInChannelForUri = (state, props) => {
return selectAllClaimsByChannel(state)[props.uri];
const byId = selectClaimsById(state);
const byChannel = selectAllClaimsByChannel(state)[props.uri] || {};
const claimIds = byChannel["all"];
if (!claimIds) return claimIds;
const claims = [];
claimIds.forEach(claimId => claims.push(byId[claimId]));
return claims;
};
export const makeSelectClaimsInChannelForUri = () => {
return createSelector(selectClaimsInChannelForUri, claims => claims);
};
export const selectClaimsInChannelForCurrentPage = (state, props) => {
const byId = selectClaimsById(state);
const byChannel = selectAllClaimsByChannel(state)[props.uri] || {};
const params = selectCurrentParams(state);
const page = params && params.page ? params.page : 1;
const claimIds = byChannel[page];
if (!claimIds) return claimIds;
const claims = [];
claimIds.forEach(claimId => claims.push(byId[claimId]));
return claims;
};
export const makeSelectClaimsInChannelForCurrentPage = () => {
return createSelector(selectClaimsInChannelForCurrentPage, claims => claims);
};
const selectMetadataForUri = (state, props) => {
const claim = selectClaimForUri(state, props);
const metadata =

View file

@ -24,3 +24,16 @@ const selectResolvingUri = (state, props) => {
export const makeSelectIsResolvingForUri = () => {
return createSelector(selectResolvingUri, resolving => resolving);
};
export const selectChannelPages = createSelector(
_selectState,
state => state.channelPages || {}
);
const selectTotalPagesForChannel = (state, props) => {
return selectChannelPages(state)[props.uri];
};
export const makeSelectTotalPagesForChannel = () => {
return createSelector(selectTotalPagesForChannel, totalPages => totalPages);
};

View file

@ -31,6 +31,7 @@
"react-dom": "^15.4.0",
"react-markdown": "^2.5.0",
"react-modal": "^1.5.2",
"react-paginate": "^4.4.3",
"react-redux": "^5.0.3",
"react-simplemde-editor": "^3.6.11",
"redux": "^3.6.0",
@ -54,8 +55,8 @@
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.18.0",
"electron-rebuild": "^1.5.11",
"css-loader": "^0.28.4",
"electron-rebuild": "^1.5.11",
"eslint": "^3.10.2",
"eslint-config-airbnb": "^13.0.0",
"eslint-loader": "^1.6.1",

View file

@ -17,6 +17,7 @@
@import "component/_modal.scss";
@import "component/_snack-bar.scss";
@import "component/_video.scss";
@import "component/_pagination.scss";
@import "page/_developer.scss";
@import "page/_reward.scss";
@import "page/_show.scss";

View file

@ -0,0 +1,32 @@
@import "../global";
.pagination {
display: block;
padding: 0;
margin: 0 auto;
text-align: center;
}
.pagination__item {
display: inline-block;
line-height: $spacing-vertical * 1.5;
height: $spacing-vertical * 1.5;
border-radius: 2px;
&:not(.pagination__item--selected):hover {
background: rgba(0, 0, 0, 0.2);
> a { cursor: hand }
}
> a {
display: inline-block;
padding: 0 $spacing-vertical * 2 / 3;
}
}
.pagination__item--previous, .pagination__item--next {
font-size: 1.2em;
}
.pagination__item--selected {
color: white;
background: $color-primary;
}

View file

@ -1204,6 +1204,10 @@ clap@^1.0.9:
dependencies:
chalk "^1.1.3"
classnames@^2.2.5:
version "2.2.5"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d"
cli-cursor@^1.0.1, cli-cursor@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987"
@ -2329,6 +2333,14 @@ faye-websocket@~0.11.0:
dependencies:
websocket-driver ">=0.5.1"
fbjs@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.2.1.tgz#622061630a43e11f845017b9044aaa648ed3f731"
dependencies:
core-js "^1.0.0"
promise "^7.0.3"
whatwg-fetch "^0.9.0"
fbjs@^0.6.1:
version "0.6.1"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.6.1.tgz#9636b7705f5ba9684d44b72f78321254afc860f7"
@ -2339,7 +2351,7 @@ fbjs@^0.6.1:
ua-parser-js "^0.7.9"
whatwg-fetch "^0.9.0"
fbjs@^0.8.9:
fbjs@^0.8.4, fbjs@^0.8.9:
version "0.8.12"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.12.tgz#10b5d92f76d45575fd63a217d4ea02bea2f8ed04"
dependencies:
@ -4771,10 +4783,26 @@ rc@^1.1.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-addons-create-fragment@^0.14.7:
version "0.14.8"
resolved "https://registry.yarnpkg.com/react-addons-create-fragment/-/react-addons-create-fragment-0.14.8.tgz#e83240d1cba49249690fcc6f148710baa11d2b7a"
react-addons-create-fragment@^15.0.0:
version "15.6.0"
resolved "https://registry.yarnpkg.com/react-addons-create-fragment/-/react-addons-create-fragment-15.6.0.tgz#af91a22b1fb095dd01f1afba43bfd0ef589d8b20"
dependencies:
fbjs "^0.8.4"
loose-envify "^1.3.1"
object-assign "^4.1.0"
react-dom-factories@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/react-dom-factories/-/react-dom-factories-1.0.0.tgz#f43c05e5051b304f33251618d5bc859b29e46b6d"
react-dom@^0.14.7:
version "0.14.9"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-0.14.9.tgz#05064a3dcf0fb1880a3b2bfc9d58c55d8d9f6293"
react-dom@^15.4.0:
version "15.6.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-15.6.1.tgz#2cb0ed4191038e53c209eb3a79a23e2a4cf99470"
@ -4804,6 +4832,15 @@ react-modal@^1.5.2:
prop-types "^15.5.7"
react-dom-factories "^1.0.0"
react-paginate@^4.4.3:
version "4.4.3"
resolved "https://registry.yarnpkg.com/react-paginate/-/react-paginate-4.4.3.tgz#11817ece628fa59c54a2df7968c854ed64b99077"
dependencies:
classnames "^2.2.5"
prop-types "^15.5.7"
react "^15.0.0"
react-addons-create-fragment "^15.0.0"
react-redux@^5.0.3:
version "5.0.5"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.5.tgz#f8e8c7b239422576e52d6b7db06439469be9846a"
@ -4823,14 +4860,20 @@ react-simplemde-editor@^3.6.11:
react "^0.14.2"
simplemde "^1.11.2"
react@^0.14.2:
react-tap-event-plugin@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/react-tap-event-plugin/-/react-tap-event-plugin-0.2.2.tgz#4f6f257851654f6c2b1c213a1d3ff21b353ae4e1"
dependencies:
fbjs "^0.2.1"
react@^0.14.2, react@^0.14.7:
version "0.14.9"
resolved "https://registry.yarnpkg.com/react/-/react-0.14.9.tgz#9110a6497c49d44ba1c0edd317aec29c2e0d91d1"
dependencies:
envify "^3.0.0"
fbjs "^0.6.1"
react@^15.4.0:
react@^15.0.0, react@^15.4.0:
version "15.6.1"
resolved "https://registry.yarnpkg.com/react/-/react-15.6.1.tgz#baa8434ec6780bde997cdc380b79cd33b96393df"
dependencies: