purchases page, cleanup on pages with <ClaimList />

This commit is contained in:
Sean Yesmunt 2020-05-11 11:54:39 -04:00
parent ca5f54cbfd
commit 919f82ba94
32 changed files with 366 additions and 142 deletions

View file

@ -132,7 +132,7 @@
"imagesloaded": "^4.1.4", "imagesloaded": "^4.1.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#259317250af62391718ac0cb8b8e25f172dc4223", "lbry-redux": "lbryio/lbry-redux#cd9c15567f2934ddc82de364d88b378ff04d5571",
"lbryinc": "lbryio/lbryinc#cc62a4eec10845cc0b31da7d0f27287cfa7c4866", "lbryinc": "lbryio/lbryinc#cc62a4eec10845cc0b31da7d0f27287cfa7c4866",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -26,7 +26,6 @@ type Props = {
// If using the default header, this is a unique ID needed to persist the state of the filter setting // If using the default header, this is a unique ID needed to persist the state of the filter setting
persistedStorageKey?: string, persistedStorageKey?: string,
showHiddenByUser: boolean, showHiddenByUser: boolean,
headerLabel?: string | Node,
showUnresolvedClaims?: boolean, showUnresolvedClaims?: boolean,
renderProperties: ?(Claim) => Node, renderProperties: ?(Claim) => Node,
includeSupportAction?: boolean, includeSupportAction?: boolean,
@ -51,7 +50,6 @@ export default function ClaimList(props: Props) {
page, page,
id, id,
showHiddenByUser, showHiddenByUser,
headerLabel,
showUnresolvedClaims, showUnresolvedClaims,
renderProperties, renderProperties,
includeSupportAction, includeSupportAction,
@ -105,12 +103,10 @@ export default function ClaimList(props: Props) {
<section <section
className={classnames('claim-list', { className={classnames('claim-list', {
'claim-list--small': type === 'small', 'claim-list--small': type === 'small',
'claim-list--card-body': isCardBody,
})} })}
> >
{header !== false && ( {header !== false && (
<React.Fragment> <React.Fragment>
{headerLabel && <label className="claim-list__header-label">{headerLabel}</label>}
{header && ( {header && (
<div className={classnames('claim-list__header', { 'section__title--small': type === 'small' })}> <div className={classnames('claim-list__header', { 'section__title--small': type === 'small' })}>
{header} {header}
@ -139,6 +135,7 @@ export default function ClaimList(props: Props) {
<ul <ul
className={classnames('ul--no-style', { className={classnames('ul--no-style', {
card: !isCardBody, card: !isCardBody,
'claim-list--card-body': isCardBody,
})} })}
> >
{sortedUris.map((uri, index) => ( {sortedUris.map((uri, index) => (

View file

@ -13,6 +13,7 @@ import ClaimPreview from 'component/claimPreview';
import { toCapitalCase } from 'util/string'; import { toCapitalCase } from 'util/string';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import Card from 'component/common/card';
type Props = { type Props = {
uris: Array<string>, uris: Array<string>,
@ -595,13 +596,18 @@ function ClaimListDiscover(props: Props) {
return ( return (
<React.Fragment> <React.Fragment>
{headerLabel && <label className="claim-list__header-label">{headerLabel}</label>}
<Card
title={header || defaultHeader}
titleActions={meta && <div className="card__actions--inline">{meta}</div>}
isBodyList
body={
<>
<ClaimList <ClaimList
isCardBody
id={claimSearchCacheQuery} id={claimSearchCacheQuery}
loading={loading} loading={loading}
uris={claimSearchResult} uris={claimSearchResult}
header={header || defaultHeader}
headerLabel={headerLabel}
headerAltControls={meta}
onScrollBottom={handleScrollBottom} onScrollBottom={handleScrollBottom}
page={page} page={page}
pageSize={CS.PAGE_SIZE} pageSize={CS.PAGE_SIZE}
@ -611,14 +617,11 @@ function ClaimListDiscover(props: Props) {
hideBlock={hideBlock} hideBlock={hideBlock}
injectedItem={injectedItem} injectedItem={injectedItem}
/> />
{loading &&
{loading && ( new Array(pageSize || CS.PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
<div className="card"> </>
{new Array(pageSize || CS.PAGE_SIZE).fill(1).map((x, i) => ( }
<ClaimPreview key={i} placeholder="loading" /> />
))}
</div>
)}
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -12,6 +12,7 @@ import {
selectChannelIsBlocked, selectChannelIsBlocked,
doFileGet, doFileGet,
makeSelectReflectingClaimForUri, makeSelectReflectingClaimForUri,
makeSelectClaimWasPurchased,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc'; import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowMatureContent } from 'redux/selectors/settings'; import { selectShowMatureContent } from 'redux/selectors/settings';
@ -36,6 +37,7 @@ const select = (state, props) => ({
channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state), channelIsBlocked: props.uri && selectChannelIsBlocked(props.uri)(state),
isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state), isSubscribed: props.uri && makeSelectIsSubscribed(props.uri, true)(state),
streamingUrl: props.uri && makeSelectStreamingUrlForUriWebProxy(props.uri)(state), streamingUrl: props.uri && makeSelectStreamingUrlForUriWebProxy(props.uri)(state),
wasPurchased: props.uri && makeSelectClaimWasPurchased(props.uri)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({

View file

@ -52,7 +52,7 @@ export default function ClaimTags(props: Props) {
} }
return ( return (
<div className={classnames('file-properties', { 'file-properties--large': type === 'large' })}> <div className={classnames('file-properties--small', { 'file-properties--large': type === 'large' })}>
{tagsToDisplay.map(tag => ( {tagsToDisplay.map(tag => (
<Tag key={tag} title={tag} name={tag} /> <Tag key={tag} title={tag} name={tag} />
))} ))}

View file

@ -13,6 +13,7 @@ type Props = {
showLBC?: boolean, showLBC?: boolean,
fee?: boolean, fee?: boolean,
badge?: boolean, badge?: boolean,
className?: string,
}; };
class CreditAmount extends React.PureComponent<Props> { class CreditAmount extends React.PureComponent<Props> {
@ -26,7 +27,18 @@ class CreditAmount extends React.PureComponent<Props> {
}; };
render() { render() {
const { amount, precision, showFullPrice, showFree, showPlus, isEstimate, fee, showLBC, badge } = this.props; const {
amount,
precision,
showFullPrice,
showFree,
showPlus,
isEstimate,
fee,
showLBC,
badge,
className,
} = this.props;
const minimumRenderableAmount = 10 ** (-1 * precision); const minimumRenderableAmount = 10 ** (-1 * precision);
const fullPrice = formatFullPrice(amount, 2); const fullPrice = formatFullPrice(amount, 2);
@ -64,13 +76,13 @@ class CreditAmount extends React.PureComponent<Props> {
return ( return (
<span <span
title={fullPrice} title={fullPrice}
className={classnames({ className={classnames(className, {
badge, badge,
'badge--cost': badge && amount > 0, 'badge--cost': badge && amount > 0,
'badge--free': badge && isFree, 'badge--free': badge && isFree,
})} })}
> >
{amountText} <span>{amountText}</span>
{isEstimate ? ( {isEstimate ? (
<span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}> <span className="credit-amount__estimate" title={__('This is an estimate and does not include data fees')}>

View file

@ -618,4 +618,9 @@ export const icons = {
viewBox: '0 0 60 60', viewBox: '0 0 60 60',
} }
), ),
[ICONS.PURCHASED]: buildIcon(
<g>
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
</g>
),
}; };

View file

@ -14,6 +14,7 @@ type Props = {
inheritStyle?: boolean, inheritStyle?: boolean,
showLBC?: boolean, showLBC?: boolean,
hideFree?: boolean, // hide the file price if it's free hideFree?: boolean, // hide the file price if it's free
className?: string,
}; };
class FilePrice extends React.PureComponent<Props> { class FilePrice extends React.PureComponent<Props> {
@ -38,7 +39,7 @@ class FilePrice extends React.PureComponent<Props> {
}; };
render() { render() {
const { costInfo, showFullPrice, badge, inheritStyle, showLBC, hideFree } = this.props; const { costInfo, showFullPrice, badge, inheritStyle, showLBC, hideFree, className } = this.props;
if (costInfo && (!costInfo.cost || (!costInfo.cost && hideFree))) { if (costInfo && (!costInfo.cost || (!costInfo.cost && hideFree))) {
return null; return null;
} }
@ -52,6 +53,7 @@ class FilePrice extends React.PureComponent<Props> {
amount={costInfo.cost} amount={costInfo.cost}
isEstimate={!costInfo.includesData} isEstimate={!costInfo.includesData}
showFullPrice={showFullPrice} showFullPrice={showFullPrice}
className={className}
/> />
) : null; ) : null;
} }

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine } from 'lbry-redux'; import { makeSelectFilePartlyDownloaded, makeSelectClaimIsMine, makeSelectClaimWasPurchased } from 'lbry-redux';
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
import FileProperties from './view'; import FileProperties from './view';
@ -8,9 +8,7 @@ const select = (state, props) => ({
isSubscribed: makeSelectIsSubscribed(props.uri)(state), isSubscribed: makeSelectIsSubscribed(props.uri)(state),
isNew: makeSelectIsNew(props.uri)(state), isNew: makeSelectIsNew(props.uri)(state),
claimIsMine: makeSelectClaimIsMine(props.uri)(state), claimIsMine: makeSelectClaimIsMine(props.uri)(state),
claimWasPurchased: makeSelectClaimWasPurchased(props.uri)(state),
}); });
export default connect( export default connect(select, null)(FileProperties);
select,
null
)(FileProperties);

View file

@ -1,5 +1,5 @@
// @flow // @flow
import * as icons from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as React from 'react'; import * as React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
@ -14,18 +14,28 @@ type Props = {
isSubscribed: boolean, isSubscribed: boolean,
isNew: boolean, isNew: boolean,
small: boolean, small: boolean,
claimWasPurchased: boolean,
}; };
export default function FileProperties(props: Props) { export default function FileProperties(props: Props) {
const { uri, downloaded, claimIsMine, isSubscribed, small = false } = props; const { uri, downloaded, claimIsMine, claimWasPurchased, isSubscribed, small = false } = props;
return ( return (
<div className={classnames('file-properties', { 'file-properties--small': small })}> <div
<FilePrice hideFree uri={uri} /> className={classnames('file-properties', {
'file-properties--small': small,
})}
>
<VideoDuration uri={uri} /> <VideoDuration uri={uri} />
<FileType uri={uri} /> <FileType uri={uri} />
{isSubscribed && <Icon tooltip icon={icons.SUBSCRIBE} />} {isSubscribed && <Icon tooltip icon={ICONS.SUBSCRIBE} />}
{!claimIsMine && downloaded && <Icon tooltip icon={icons.LIBRARY} />} {!claimIsMine && downloaded && <Icon tooltip icon={ICONS.LIBRARY} />}
{claimWasPurchased ? (
<span className="file-properties__purchased">
<Icon icon={ICONS.PURCHASED} />
</span>
) : (
<FilePrice hideFree uri={uri} badge={false} className="file-properties__not-purchased" />
)}
</div> </div>
); );
} }

View file

@ -14,7 +14,6 @@ import DiscoverPage from 'page/discover';
import HomePage from 'page/home'; import HomePage from 'page/home';
import InvitedPage from 'page/invited'; import InvitedPage from 'page/invited';
import RewardsPage from 'page/rewards'; import RewardsPage from 'page/rewards';
import FileListDownloaded from 'page/fileListDownloaded';
import FileListPublished from 'page/fileListPublished'; import FileListPublished from 'page/fileListPublished';
import InvitePage from 'page/invite'; import InvitePage from 'page/invite';
import SearchPage from 'page/search'; import SearchPage from 'page/search';
@ -177,7 +176,6 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} /> <Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} /> <PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} />
<PrivateRoute {...props} path={`/$/${PAGES.DOWNLOADED}`} component={FileListDownloaded} />
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISHED}`} component={FileListPublished} /> <PrivateRoute {...props} path={`/$/${PAGES.PUBLISHED}`} component={FileListPublished} />
<PrivateRoute {...props} path={`/$/${PAGES.CREATOR_DASHBOARD}`} component={CreatorDashboard} /> <PrivateRoute {...props} path={`/$/${PAGES.CREATOR_DASHBOARD}`} component={CreatorDashboard} />
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISH}`} component={PublishPage} /> <PrivateRoute {...props} path={`/$/${PAGES.PUBLISH}`} component={PublishPage} />

View file

@ -3,7 +3,7 @@ export const MINIMUM_PUBLISH_BID = 0.00001;
export const CHANNEL_ANONYMOUS = 'anonymous'; export const CHANNEL_ANONYMOUS = 'anonymous';
export const CHANNEL_NEW = 'new'; export const CHANNEL_NEW = 'new';
export const PAGE_SIZE = 20; export const PAGE_SIZE = 20;
export const PUBLISHES_PAGE_SIZE = 10; export const MY_CLAIMS_PAGE_SIZE = 10;
export const PAGE_PARAM = 'page'; export const PAGE_PARAM = 'page';
export const PAGE_SIZE_PARAM = 'page_size'; export const PAGE_SIZE_PARAM = 'page_size';

View file

@ -99,3 +99,4 @@ export const VALIDATED = 'Check';
export const SLIDERS = 'Sliders'; export const SLIDERS = 'Sliders';
export const SCIENCE = 'Science'; export const SCIENCE = 'Science';
export const ANALYTICS = 'BarChart2'; export const ANALYTICS = 'BarChart2';
export const PURCHASED = 'Key';

View file

@ -6,7 +6,6 @@ exports.BACKUP = 'backup';
exports.CHANNEL = 'channel'; exports.CHANNEL = 'channel';
exports.DISCOVER = 'discover'; exports.DISCOVER = 'discover';
exports.HOME = 'home'; exports.HOME = 'home';
exports.DOWNLOADED = 'downloaded';
exports.HELP = 'help'; exports.HELP = 'help';
exports.LIBRARY = 'library'; exports.LIBRARY = 'library';
exports.INVITE = 'invite'; exports.INVITE = 'invite';

View file

@ -2,6 +2,9 @@
import React from 'react'; import React from 'react';
import FilePrice from 'component/filePrice'; import FilePrice from 'component/filePrice';
import { Modal } from 'modal/modal'; import { Modal } from 'modal/modal';
import Card from 'component/common/card';
import I18nMessage from 'component/i18nMessage';
import Button from 'component/button';
type Props = { type Props = {
closeModal: () => void, closeModal: () => void,
@ -30,22 +33,29 @@ class ModalAffirmPurchase extends React.PureComponent<Props> {
uri, uri,
} = this.props; } = this.props;
const modalTitle = __('Confirm Purchase');
return ( return (
<Modal <Modal type="card" isOpen contentLabel={modalTitle} onAborted={cancelPurchase}>
type="confirm" <Card
isOpen title={modalTitle}
title={__('Confirm Purchase')} subtitle={
contentLabel={__('Confirm Purchase')} <I18nMessage
onConfirmed={this.onAffirmPurchase} tokens={{
onAborted={cancelPurchase} claim_title: <strong>{title ? `"${title}"` : uri}</strong>,
amount: <FilePrice uri={uri} showFullPrice inheritStyle />,
}}
> >
<p className="section__subtitle"> This will purchase %claim_title% for %amount%.
{__('This will purchase')} <strong>{title ? `"${title}"` : uri}</strong> {__('for')}{' '} </I18nMessage>
<strong> }
<FilePrice uri={uri} showFullPrice inheritStyle showLBC={false} /> actions={
</strong>{' '} <div className="section__actions">
{__('credits')}. <Button button="primary" label={__('Confirm')} onClick={this.onAffirmPurchase} />
</p> <Button button="link" label={__('Cancel')} onClick={cancelPurchase} />
</div>
}
/>
</Modal> </Modal>
); );
} }

View file

@ -33,8 +33,8 @@ function ChannelsFollowingPage(props: Props) {
meta={ meta={
<Button <Button
icon={ICONS.SEARCH} icon={ICONS.SEARCH}
button="link" button="alt"
label={__('Discover New Channels')} label={__('Discover Channels')}
navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`} navigate={`/$/${PAGES.CHANNELS_FOLLOWING_DISCOVER}`}
/> />
} }

View file

@ -1,5 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectSearchDownloadUrlsForPage, makeSelectSearchDownloadUrlsCount, selectDownloadUrlsCount, selectIsFetchingFileList } from 'lbry-redux'; import {
makeSelectSearchDownloadUrlsForPage,
selectDownloadUrlsCount,
selectIsFetchingFileList,
doPurchaseList,
makeSelectMyPurchasesForPage,
selectIsFetchingMyPurchases,
selectMyPurchasesCount,
} from 'lbry-redux';
import FileListDownloaded from './view'; import FileListDownloaded from './view';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
@ -13,16 +21,17 @@ const select = (state, props) => {
page, page,
history, history,
query, query,
allDownloadedUrlsCount: selectDownloadUrlsCount(state), downloadedUrlsCount: selectDownloadUrlsCount(state),
downloadedUrls: makeSelectSearchDownloadUrlsForPage(query, page)(state), myPurchasesCount: selectMyPurchasesCount(state),
downloadedUrlsCount: makeSelectSearchDownloadUrlsCount(query)(state), myPurchases: makeSelectMyPurchasesForPage(query, page)(state),
fetching: selectIsFetchingFileList(state), myDownloads: makeSelectSearchDownloadUrlsForPage(query, page)(state),
fetchingFileList: selectIsFetchingFileList(state),
fetchingMyPurchases: selectIsFetchingMyPurchases(state),
}; };
}; };
export default withRouter( export default withRouter(
connect( connect(select, {
select, doPurchaseList,
null })(FileListDownloaded)
)(FileListDownloaded)
); );

View file

@ -1,4 +1,5 @@
// @flow // @flow
import * as ICONS from 'constants/icons';
import React, { useState } from 'react'; import React, { useState } from 'react';
import Button from 'component/button'; import Button from 'component/button';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
@ -6,24 +7,41 @@ import Paginate from 'component/common/paginate';
import { PAGE_SIZE } from 'constants/claim'; import { PAGE_SIZE } from 'constants/claim';
import { Form } from 'component/common/form-components/form'; import { Form } from 'component/common/form-components/form';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import * as ICONS from '../../constants/icons'; import { FormField } from 'component/common/form-components/form-field';
import { FormField } from '../../component/common/form-components/form-field';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import Card from 'component/common/card';
import classnames from 'classnames';
type Props = { type Props = {
fetching: boolean, fetchingFileList: boolean,
allDownloadedUrlsCount: number,
downloadedUrls: Array<string>, downloadedUrls: Array<string>,
downloadedUrlsCount: ?number, downloadedUrlsCount: ?number,
history: { replace: string => void }, history: { replace: string => void },
page: number,
query: string, query: string,
doPurchaseList: () => void,
myDownloads: Array<string>,
myPurchases: Array<string>,
myPurchasesCount: ?number,
fetchingMyPurchases: boolean,
}; };
function FileListDownloaded(props: Props) { const VIEW_DOWNLOADS = 'view_download';
const { fetching, history, query, allDownloadedUrlsCount, downloadedUrls, downloadedUrlsCount } = props; const VIEW_PURCHASES = 'view_purchases';
const hasDownloads = allDownloadedUrlsCount > 0;
function FileListDownloaded(props: Props) {
const {
history,
query,
downloadedUrlsCount,
myPurchasesCount,
myPurchases,
myDownloads,
fetchingFileList,
fetchingMyPurchases,
doPurchaseList,
} = props;
const loading = fetchingFileList || fetchingMyPurchases;
const [viewMode, setViewMode] = React.useState(VIEW_PURCHASES);
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
function handleInputChange(e) { function handleInputChange(e) {
@ -34,13 +52,36 @@ function FileListDownloaded(props: Props) {
} }
} }
React.useEffect(() => {
doPurchaseList();
}, [doPurchaseList]);
return ( return (
<React.Fragment> <Card
{hasDownloads ? ( title={
<React.Fragment> <div>
<ClaimList <Button
header={__('Your Library')} icon={ICONS.LIBRARY}
headerAltControls={ button="alt"
label={__('All Downloads')}
className={classnames(`button-toggle`, {
'button-toggle--active': viewMode === VIEW_DOWNLOADS,
})}
onClick={() => setViewMode(VIEW_DOWNLOADS)}
/>
<Button
icon={ICONS.PURCHASED}
button="alt"
label={__('Your Purchases')}
className={classnames(`button-toggle`, {
'button-toggle--active': viewMode === VIEW_PURCHASES,
})}
onClick={() => setViewMode(VIEW_PURCHASES)}
/>
</div>
}
titleActions={
<div className="card__actions--inline">
<Form onSubmit={() => {}} className="wunderbar--inline"> <Form onSubmit={() => {}} className="wunderbar--inline">
<Icon icon={ICONS.SEARCH} /> <Icon icon={ICONS.SEARCH} />
<FormField <FormField
@ -52,27 +93,35 @@ function FileListDownloaded(props: Props) {
placeholder={__('Search')} placeholder={__('Search')}
/> />
</Form> </Form>
</div>
} }
persistedStorageKey="claim-list-downloaded" isBodyList
empty={__('No results for %query%', { query })} body={
uris={downloadedUrls} <div>
loading={fetching} <ClaimList
/> isCardBody
<Paginate totalPages={Math.ceil(Number(downloadedUrlsCount) / Number(PAGE_SIZE))} loading={fetching} /> renderProperties={() => null}
</React.Fragment> empty={
viewMode === VIEW_PURCHASES && !query ? (
<div>{__("You haven't purchased anything yet silly goose.")}</div>
) : ( ) : (
<div className="main--empty"> __('No results for %query%', { query })
<section className="card card--section"> )
<h2 className="card__title card__title--deprecated"> }
{__("You haven't downloaded anything from LBRY yet.")} uris={viewMode === VIEW_PURCHASES ? myPurchases : myDownloads}
</h2> loading={loading}
<div className="card__actions card__actions--center"> />
<Button button="primary" navigate="/" label={__('Explore new content')} /> {!query && (
</div> <Paginate
</section> loading={loading}
</div> totalPages={Math.ceil(
Number(viewMode === VIEW_PURCHASES ? myPurchasesCount : downloadedUrlsCount) / Number(PAGE_SIZE)
)} )}
</React.Fragment> />
)}
</div>
}
/>
); );
} }

View file

@ -11,13 +11,13 @@ import { selectUploadCount } from 'lbryinc';
import { doCheckPendingPublishesApp } from 'redux/actions/publish'; import { doCheckPendingPublishesApp } from 'redux/actions/publish';
import FileListPublished from './view'; import FileListPublished from './view';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import { PUBLISHES_PAGE_SIZE, PAGE_PARAM, PAGE_SIZE_PARAM } from 'constants/claim'; import { MY_CLAIMS_PAGE_SIZE, PAGE_PARAM, PAGE_SIZE_PARAM } from 'constants/claim';
const select = (state, props) => { const select = (state, props) => {
const { search } = props.location; const { search } = props.location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const page = Number(urlParams.get(PAGE_PARAM)) || '1'; const page = Number(urlParams.get(PAGE_PARAM)) || '1';
const pageSize = urlParams.get(PAGE_SIZE_PARAM) || String(PUBLISHES_PAGE_SIZE); const pageSize = urlParams.get(PAGE_SIZE_PARAM) || String(MY_CLAIMS_PAGE_SIZE);
return { return {
page, page,

View file

@ -1,3 +1,17 @@
import { connect } from 'react-redux';
import {
selectDownloadUrlsCount,
selectIsFetchingFileList,
selectMyPurchases,
selectIsFetchingMyPurchases,
} from 'lbry-redux';
import LibraryPage from './view'; import LibraryPage from './view';
export default LibraryPage; const select = state => ({
allDownloadedUrlsCount: selectDownloadUrlsCount(state),
fetchingFileList: selectIsFetchingFileList(state),
myPurchases: selectMyPurchases(state),
fetchingMyPurchases: selectIsFetchingMyPurchases(state),
});
export default connect(select)(LibraryPage);

View file

@ -1,12 +1,45 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import Button from 'component/button';
import Page from 'component/page'; import Page from 'component/page';
import Spinner from 'component/spinner';
import DownloadList from 'page/fileListDownloaded'; import DownloadList from 'page/fileListDownloaded';
import Yrbl from 'component/yrbl';
type Props = {
allDownloadedUrlsCount: number,
myPurchases: Array<string>,
fetchingMyPurchases: boolean,
fetchingFileList: boolean,
};
function LibraryPage(props: Props) {
const { allDownloadedUrlsCount, myPurchases, fetchingMyPurchases, fetchingFileList } = props;
const hasDownloads = allDownloadedUrlsCount > 0 || (myPurchases && myPurchases.length);
const loading = fetchingFileList || fetchingMyPurchases;
function LibraryPage() {
return ( return (
<Page> <Page>
<DownloadList /> {loading && !hasDownloads && (
<div className="main--empty">
<Spinner delayed />
</div>
)}
{!loading && !hasDownloads && (
<div className="main--empty">
<Yrbl
title={__("You haven't downloaded anything from LBRY yet")}
subtitle={
<div className="section__actions">
<Button button="primary" navigate="/" label={__('Explore new content')} />
</div>
}
/>
</div>
)}
{hasDownloads && <DownloadList />}
</Page> </Page>
); );
} }

View file

@ -22,7 +22,7 @@ function DiscoverPage() {
defaultTags={CS.TAGS_FOLLOWED} defaultTags={CS.TAGS_FOLLOWED}
meta={ meta={
<Button <Button
button="link" button="alt"
icon={ICONS.EDIT} icon={ICONS.EDIT}
label={__('Manage')} label={__('Manage')}
requiresAuth={IS_WEB} requiresAuth={IS_WEB}

View file

@ -45,7 +45,8 @@ export const makeSelectIsPlayerFloating = (location: UrlLocation) =>
// If there is no floatingPlayer explicitly set, see if the playingUri can float // If there is no floatingPlayer explicitly set, see if the playingUri can float
try { try {
const { pathname } = location; const { pathname } = location;
const pageUrl = buildURI(parseURI(pathname.slice(1).replace(/:/g, '#'))); const { streamName, streamClaimId, channelName, channelClaimId } = parseURI(pathname.slice(1).replace(/:/g, '#'));
const pageUrl = buildURI({ streamName, streamClaimId, channelName, channelClaimId });
const claimFromUrl = claimsByUri[pageUrl]; const claimFromUrl = claimsByUri[pageUrl];
const playingClaim = claimsByUri[playingUri]; const playingClaim = claimsByUri[playingUri];
return (claimFromUrl && claimFromUrl.claim_id) !== (playingClaim && playingClaim.claim_id); return (claimFromUrl && claimFromUrl.claim_id) !== (playingClaim && playingClaim.claim_id);

View file

@ -4,12 +4,15 @@
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
font-size: var(--font-small); font-size: var(--font-small);
} }
}
@media (min-width: $breakpoint-small) { .button--primary,
.button--secondary,
.button--alt,
.button--link {
&:focus { &:focus {
@include focus; @include focus;
z-index: 2; z-index: 2; // Ensure focus box-shadow is always visible on every button side
}
} }
} }
@ -48,6 +51,10 @@
background-color: var(--color-button-primary-bg); background-color: var(--color-button-primary-bg);
} }
&:focus {
@include focus;
}
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
padding: var(--spacing-medium) var(--spacing-small); padding: var(--spacing-medium) var(--spacing-small);
} }
@ -124,6 +131,7 @@ svg + .button__label,
&:hover { &:hover {
cursor: default; cursor: default;
text-decoration: none; text-decoration: none;
background-color: var(--color-primary-alt);
} }
} }

View file

@ -2,28 +2,30 @@
background-color: var(--color-card-background); background-color: var(--color-card-background);
position: relative; position: relative;
display: flex; display: flex;
flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
padding: var(--spacing-medium); padding: var(--spacing-medium);
padding-bottom: var(--spacing-small); padding-bottom: var(--spacing-small);
margin-top: var(--spacing-medium); margin-top: var(--spacing-medium);
color: var(--color-text-subtitle);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
color: var(--color-text-subtitle);
} }
.claim-search__dropdown { .claim-search__dropdown {
padding: 0 var(--spacing-medium); padding: 0 var(--spacing-medium);
max-width: 400px; max-width: 400px;
font-size: var(--font-body);
background-color: var(--color-card-background);
width: var(--option-select-width);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
margin-left: 0; margin-left: 0;
} }
background-color: var(--color-card-background);
width: var(--option-select-width);
} }
.claim-search__dropdown--selected { .claim-search__dropdown--selected {
@ -51,13 +53,11 @@
.claim-search__extra { .claim-search__extra {
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
} }
.claim-search__top { .claim-search__top {
display: flex; display: flex;
flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
} }

View file

@ -4,7 +4,7 @@
align-items: center; align-items: center;
font-size: var(--font-small); font-size: var(--font-small);
color: var(--color-text-help); color: var(--color-text-help);
margin-left: var(--spacing-small); margin-left: var(--spacing-medium);
white-space: nowrap; white-space: nowrap;
& > *:not(:last-child) { & > *:not(:last-child) {
@ -20,12 +20,68 @@
font-size: var(--font-xsmall); font-size: var(--font-xsmall);
line-height: 1.2; line-height: 1.2;
margin-left: 0; margin-left: 0;
position: relative;
.tag {
font-size: var(--font-xsmall);
}
& > *:not(:last-child) { & > *:not(:last-child) {
margin-right: var(--spacing-miniscule); margin-right: var(--spacing-miniscule);
} }
} }
.file-properties__purchased {
position: relative;
display: flex;
align-items: center;
margin-left: var(--spacing-xsmall);
color: var(--color-gray-5);
span,
svg {
position: relative;
fill: white;
}
.icon {
margin-left: 0.5rem;
}
&::before {
position: absolute;
content: '';
left: -0.4rem;
right: -5rem;
height: 1.75rem;
transform: skew(20deg);
background-color: var(--color-purchased);
}
}
.file-properties__not-purchased {
position: relative;
display: flex;
align-items: center;
color: var(--color-purchased-text);
span {
position: relative;
margin-left: 0.75rem;
}
&::before {
position: absolute;
content: '';
left: 0;
right: -5rem;
height: 1.75rem;
background-color: var(--color-purchased-alt);
border: 2px solid var(--color-purchased);
transform: skew(20deg);
}
}
.file-properties--large { .file-properties--large {
flex-wrap: wrap; flex-wrap: wrap;
margin-bottom: var(--spacing-large); margin-bottom: var(--spacing-large);

View file

@ -5,6 +5,10 @@
input-submit { input-submit {
align-items: center; align-items: center;
input {
z-index: 2;
}
} }
input[type='number'] { input[type='number'] {

View file

@ -71,3 +71,9 @@
.pagination__item--selected { .pagination__item--selected {
background-color: var(--color-button-secondary-bg); background-color: var(--color-button-secondary-bg);
} }
.paginate-channel {
width: 5rem;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}

View file

@ -42,7 +42,6 @@ p {
ul, ul,
ol { ol {
li { li {
list-style-position: outside; list-style-position: outside;
margin: var(--spacing-xsmall) var(--spacing-medium); margin: var(--spacing-xsmall) var(--spacing-medium);
@ -218,8 +217,10 @@ img {
color: var(--color-text-empty); color: var(--color-text-empty);
font-style: italic; font-style: italic;
} }
.empty--centered { .empty--centered {
text-align: center; text-align: center;
padding: calc(var(--spacing-large) * 3) 0;
} }
.qr-code { .qr-code {

View file

@ -43,6 +43,9 @@
--color-comment-menu-hovering: #e0e0e0; --color-comment-menu-hovering: #e0e0e0;
--color-notice: #58563b; --color-notice: #58563b;
--color-error: #61373f; --color-error: #61373f;
--color-purchased: #ffd580;
--color-purchased-alt: #ffd5804a;
--color-purchased-text: #eeeeee;
// Text // Text
--color-text: #eeeeee; --color-text: #eeeeee;

View file

@ -15,6 +15,9 @@
--color-comment-menu: #e0e0e0; --color-comment-menu: #e0e0e0;
--color-comment-menu-hovering: #6a6a6a; --color-comment-menu-hovering: #6a6a6a;
--color-notice: #fef3ca; --color-notice: #fef3ca;
--color-purchased: #ffd580;
--color-purchased-alt: #ffebc2;
--color-purchased-text: var(--color-gray-5);
// Icons // Icons
--color-follow-bg: #ffd4da; --color-follow-bg: #ffd4da;

View file

@ -6178,9 +6178,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#259317250af62391718ac0cb8b8e25f172dc4223: lbry-redux@lbryio/lbry-redux#cd9c15567f2934ddc82de364d88b378ff04d5571:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/259317250af62391718ac0cb8b8e25f172dc4223" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/cd9c15567f2934ddc82de364d88b378ff04d5571"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"