Add tags to publish page, infinite scroll, navigation improvements #2593
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "LBRY",
|
||||
"version": "0.33.1",
|
||||
"version": "0.33.2",
|
||||
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
|
||||
"keywords": [
|
||||
"lbry"
|
||||
|
@ -123,7 +123,7 @@
|
|||
"jsmediatags": "^3.8.1",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#141593500693a93db74c62ef5a9fe67b43896603",
|
||||
"lbry-redux": "lbryio/lbry-redux#2930ad82a90ca91f6caf3761597ef9a67da7db66",
|
||||
"lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { hot } from 'react-hot-loader/root';
|
||||
import { connect } from 'react-redux';
|
||||
import { doUpdateBlockHeight, doError } from 'lbry-redux';
|
||||
import { doUpdateBlockHeight, doError, doFetchTransactions } from 'lbry-redux';
|
||||
import { selectUser, doRewardList, doFetchRewardedContent } from 'lbryinc';
|
||||
import { selectThemePath } from 'redux/selectors/settings';
|
||||
import App from './view';
|
||||
|
@ -15,6 +15,7 @@ const perform = dispatch => ({
|
|||
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||
});
|
||||
|
||||
export default hot(
|
||||
|
|
|
@ -9,6 +9,8 @@ import { openContextMenu } from 'util/context-menu';
|
|||
import useKonamiListener from 'util/enhanced-layout';
|
||||
import Yrbl from 'component/yrbl';
|
||||
|
||||
export const MAIN_WRAPPER_CLASS = 'main-wrapper';
|
||||
|
||||
type Props = {
|
||||
alertError: (string | {}) => void,
|
||||
pageTitle: ?string,
|
||||
|
@ -16,18 +18,23 @@ type Props = {
|
|||
theme: string,
|
||||
fetchRewards: () => void,
|
||||
fetchRewardedContent: () => void,
|
||||
fetchTransactions: () => void,
|
||||
};
|
||||
|
||||
function App(props: Props) {
|
||||
const { theme, fetchRewards, fetchRewardedContent } = props;
|
||||
const { theme, fetchRewards, fetchRewardedContent, fetchTransactions } = props;
|
||||
const appRef = useRef();
|
||||
const isEnhancedLayout = useKonamiListener();
|
||||
|
||||
useEffect(() => {
|
||||
ReactModal.setAppElement(appRef.current);
|
||||
fetchRewards();
|
||||
fetchRewardedContent();
|
||||
}, [fetchRewards, fetchRewardedContent]);
|
||||
|
||||
// @if TARGET='app'
|
||||
fetchRewards();
|
||||
fetchTransactions();
|
||||
// @endif
|
||||
}, [fetchRewards, fetchRewardedContent, fetchTransactions]);
|
||||
|
||||
useEffect(() => {
|
||||
// $FlowFixMe
|
||||
|
@ -38,7 +45,7 @@ function App(props: Props) {
|
|||
<div ref={appRef} onContextMenu={e => openContextMenu(e)}>
|
||||
<Header />
|
||||
|
||||
<div className="main-wrapper">
|
||||
<div className={MAIN_WRAPPER_CLASS}>
|
||||
<div className="main-wrapper-inner">
|
||||
<Router />
|
||||
<SideBar />
|
||||
|
|
|
@ -35,7 +35,7 @@ function ChannelContent(props: Props) {
|
|||
|
||||
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />}
|
||||
|
||||
{hasContent && <ClaimList header={false} uris={claimsInChannel.map(claim => claim.permanent_url)} />}
|
||||
{hasContent && <ClaimList header={false} uris={claimsInChannel.map(claim => claim.permanent_url).reverse()} />}
|
||||
|
||||
<Paginate
|
||||
onPageChange={page => fetchClaims(uri, page)}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// @flow
|
||||
import { MAIN_WRAPPER_CLASS } from 'component/app/view';
|
||||
import type { Node } from 'react';
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import Spinner from 'component/spinner';
|
||||
|
@ -19,14 +20,25 @@ type Props = {
|
|||
type: string,
|
||||
empty?: string,
|
||||
meta?: Node,
|
||||
onScrollBottom?: any => void,
|
||||
// If using the default header, this is a unique ID needed to persist the state of the filter setting
|
||||
persistedStorageKey?: string,
|
||||
};
|
||||
|
||||
export default function ClaimList(props: Props) {
|
||||
const { uris, headerAltControls, injectedItem, loading, persistedStorageKey, empty, meta, type, header } = props;
|
||||
const {
|
||||
uris,
|
||||
headerAltControls,
|
||||
injectedItem,
|
||||
loading,
|
||||
persistedStorageKey,
|
||||
empty,
|
||||
meta,
|
||||
type,
|
||||
header,
|
||||
onScrollBottom,
|
||||
} = props;
|
||||
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
|
||||
const sortedUris = uris && currentSort === SORT_NEW ? uris.slice().reverse() : uris;
|
||||
const hasUris = uris && !!uris.length;
|
||||
const sortedUris = (hasUris && (currentSort === SORT_NEW ? uris : uris.slice().reverse())) || [];
|
||||
|
||||
|
@ -34,8 +46,31 @@ export default function ClaimList(props: Props) {
|
|||
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
|
||||
}
|
||||
It could potentially make sense to move this out of here and instead have the scroll state be something that is generally consumable by any component (i.e. track the scroll state app-wide). I'm not up to date on the best techniques for this though. It could potentially make sense to move this out of here and instead have the scroll state be something that is generally consumable by any component (i.e. track the scroll state app-wide).
I'm not up to date on the best techniques for this though.
|
||||
|
||||
useEffect(() => {
|
||||
if (onScrollBottom) {
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}
|
||||
}, [loading, handleScroll]);
|
||||
|
||||
function handleScroll(e) {
|
||||
if (onScrollBottom) {
|
||||
const x = document.querySelector(`.${MAIN_WRAPPER_CLASS}`);
|
||||
|
||||
if (x && window.scrollY + window.innerHeight >= x.offsetHeight) {
|
||||
// fix this
|
||||
if (!loading && uris.length > 19) {
|
||||
onScrollBottom();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={classnames('file-list')}>
|
||||
<section className={classnames('claim-list')}>
|
||||
{header !== false && (
|
||||
<div className={classnames('claim-list__header', { 'claim-list__header--small': type === 'small' })}>
|
||||
{header || (
|
||||
|
@ -60,7 +95,7 @@ export default function ClaimList(props: Props) {
|
|||
{sortedUris.map((uri, index) => (
|
||||
<React.Fragment key={uri}>
|
||||
<ClaimPreview uri={uri} type={type} />
|
||||
{index === 4 && injectedItem && <li className="claim-list__item--injected">{injectedItem}</li>}
|
||||
{index === 4 && injectedItem && <li className="claim-preview--injected">{injectedItem}</li>}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
// @flow
|
||||
import type { Node } from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import moment from 'moment';
|
||||
import usePersistedState from 'util/use-persisted-state';
|
||||
import { FormField } from 'component/common/form';
|
||||
import ClaimList from 'component/claimList';
|
||||
import Tag from 'component/tag';
|
||||
import usePersistedState from 'util/use-persisted-state';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
const TIME_DAY = 'day';
|
||||
const TIME_WEEK = 'week';
|
||||
const TIME_MONTH = 'month';
|
||||
|
@ -37,11 +39,17 @@ function ClaimListDiscover(props: Props) {
|
|||
const [personalSort, setPersonalSort] = usePersistedState('file-list-trending:personalSort', SEARCH_SORT_YOU);
|
||||
const [typeSort, setTypeSort] = usePersistedState('file-list-trending:typeSort', TYPE_TRENDING);
|
||||
const [timeSort, setTimeSort] = usePersistedState('file-list-trending:timeSort', TIME_WEEK);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const toCapitalCase = string => string.charAt(0).toUpperCase() + string.slice(1);
|
||||
const tagsString = tags.join(',');
|
||||
useEffect(() => {
|
||||
const options = {};
|
||||
const options: {
|
||||
page_size: number,
|
||||
any_tags?: Array<string>,
|
||||
order_by?: Array<string>,
|
||||
release_time?: string,
|
||||
} = { page_size: PAGE_SIZE, page };
|
||||
const newTags = tagsString.split(',');
|
||||
|
||||
if ((newTags && !personal) || (newTags && personal && personalSort === SEARCH_SORT_YOU)) {
|
||||
|
@ -65,7 +73,7 @@ function ClaimListDiscover(props: Props) {
|
|||
}
|
||||
|
||||
doClaimSearch(20, options);
|
||||
}, [personal, personalSort, typeSort, timeSort, doClaimSearch, tagsString]);
|
||||
}, [personal, personalSort, typeSort, timeSort, doClaimSearch, page, tagsString]);
|
||||
|
||||
const header = (
|
||||
<h1 className="card__title--flex">
|
||||
|
@ -91,7 +99,10 @@ function ClaimListDiscover(props: Props) {
|
|||
name="trending_overview"
|
||||
className="claim-list__dropdown"
|
||||
value={personalSort}
|
||||
onChange={e => setPersonalSort(e.target.value)}
|
||||
onChange={e => {
|
||||
setPage(1);
|
||||
setPersonalSort(e.target.value);
|
||||
}}
|
||||
>
|
||||
{SEARCH_FILTER_TYPES.map(type => (
|
||||
<option key={type} value={type}>
|
||||
|
@ -132,7 +143,10 @@ function ClaimListDiscover(props: Props) {
|
|||
injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem}
|
||||
header={header}
|
||||
headerAltControls={headerAltControls}
|
||||
onScrollBottom={() => setPage(page + 1)}
|
||||
/>
|
||||
|
||||
{loading && page > 1 && new Array(PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder />)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import React, { useEffect } from 'react';
|
||||
import classnames from 'classnames';
|
||||
import { convertToShareLink } from 'lbry-redux';
|
||||
import { parseURI, convertToShareLink } from 'lbry-redux';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { openCopyLinkMenu } from 'util/context-menu';
|
||||
import { formatLbryUriForWeb } from 'util/uri';
|
||||
|
@ -53,8 +53,8 @@ function ClaimPreview(props: Props) {
|
|||
blackListedOutpoints,
|
||||
} = props;
|
||||
const haventFetched = claim === undefined;
|
||||
const abandoned = !isResolvingUri && !claim;
|
||||
const isChannel = claim && claim.value_type === 'channel';
|
||||
const abandoned = !isResolvingUri && !claim && !placeholder;
|
||||
const { isChannel } = parseURI(uri);
|
||||
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
|
||||
let shouldHide = abandoned || (!claimIsMine && obscureNsfw && nsfw);
|
||||
|
||||
|
@ -94,10 +94,10 @@ function ClaimPreview(props: Props) {
|
|||
|
||||
if (placeholder && !claim) {
|
||||
return (
|
||||
<li className="claim-list__item" disabled>
|
||||
<li className="claim-preview" disabled>
|
||||
<div className="placeholder media__thumb" />
|
||||
<div className="placeholder__wrapper">
|
||||
<div className="placeholder claim-list__item-title" />
|
||||
<div className="placeholder claim-preview-title" />
|
||||
<div className="placeholder media__subtitle" />
|
||||
</div>
|
||||
</li>
|
||||
|
@ -109,15 +109,15 @@ function ClaimPreview(props: Props) {
|
|||
role="link"
|
||||
onClick={pending ? undefined : onClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={classnames('claim-list__item', {
|
||||
'claim-list__item--large': type === 'large',
|
||||
className={classnames('claim-preview', {
|
||||
'claim-preview--large': type === 'large',
|
||||
'claim-list__pending': pending,
|
||||
})}
|
||||
>
|
||||
{isChannel ? <ChannelThumbnail uri={uri} /> : <CardMedia thumbnail={thumbnail} />}
|
||||
<div className="claim-list__item-metadata">
|
||||
<div className="claim-list__item-info">
|
||||
<div className="claim-list__item-title">
|
||||
<div className="claim-preview-metadata">
|
||||
<div className="claim-preview-info">
|
||||
<div className="claim-preview-title">
|
||||
<TruncatedText text={title || (claim && claim.name)} lines={1} />
|
||||
</div>
|
||||
{type !== 'small' && (
|
||||
|
@ -128,7 +128,7 @@ function ClaimPreview(props: Props) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
<div className="claim-list__item-properties">
|
||||
<div className="claim-preview-properties">
|
||||
<div className="media__subtitle">
|
||||
<UriIndicator uri={uri} link />
|
||||
{pending && <div>Pending...</div>}
|
||||
|
|
|
@ -40,6 +40,12 @@ export const icons = {
|
|||
<path d="M294.3,150.9l2-12.6l-12.2-2.1l0.8-4.9l17.1,2.9l-2.8,17.5L294.3,150.9L294.3,150.9z" />
|
||||
</svg>
|
||||
),
|
||||
[ICONS.FEATURED]: buildIcon(
|
||||
<g fill="none" fillRule="evenodd" strokeLinecap="round">
|
||||
<circle cx="12" cy="8" r="7" />
|
||||
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.ARROW_LEFT]: buildIcon(
|
||||
<g fill="none" fillRule="evenodd" strokeLinecap="round">
|
||||
<path d="M4, 12 L21, 12" />
|
||||
|
@ -210,4 +216,12 @@ export const icons = {
|
|||
[ICONS.CHAT]: buildIcon(
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||
),
|
||||
[ICONS.YES]: buildIcon(
|
||||
<path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3" />
|
||||
),
|
||||
[ICONS.NO]: buildIcon(
|
||||
<path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17" />
|
||||
),
|
||||
[ICONS.UP]: buildIcon(<polyline points="18 15 12 9 6 15" />),
|
||||
[ICONS.DOWN]: buildIcon(<polyline points="6 9 12 15 18 9" />),
|
||||
};
|
||||
|
|
|
@ -42,7 +42,6 @@ class FileDetails extends PureComponent<Props> {
|
|||
<Expandable>
|
||||
{description && (
|
||||
<Fragment>
|
||||
<div className="media__info-title">About</div>
|
||||
<div className="media__info-text">
|
||||
<MarkdownPreview content={description} promptLinks />
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectFileInfoForUri, makeSelectClaimIsMine } from 'lbry-redux';
|
||||
import { makeSelectFileInfoForUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
|
||||
import { selectRewardContentClaimIds } from 'lbryinc';
|
||||
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
|
||||
import FileProperties from './view';
|
||||
|
@ -10,6 +10,7 @@ const select = (state, props) => ({
|
|||
isSubscribed: makeSelectIsSubscribed(props.uri)(state),
|
||||
isNew: makeSelectIsNew(props.uri)(state),
|
||||
claimIsMine: makeSelectClaimIsMine(props.uri)(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
|
|
|
@ -7,6 +7,7 @@ import FilePrice from 'component/filePrice';
|
|||
|
||||
type Props = {
|
||||
uri: string,
|
||||
claim: ?StreamClaim,
|
||||
downloaded: boolean,
|
||||
claimIsMine: boolean,
|
||||
isSubscribed: boolean,
|
||||
|
@ -15,16 +16,32 @@ type Props = {
|
|||
};
|
||||
|
||||
export default function FileProperties(props: Props) {
|
||||
const { uri, downloaded, claimIsMine, rewardedContentClaimIds, isSubscribed } = props;
|
||||
const { claim, uri, downloaded, claimIsMine, rewardedContentClaimIds, isSubscribed } = props;
|
||||
const { claimId } = parseURI(uri);
|
||||
const isRewardContent = rewardedContentClaimIds.includes(claimId);
|
||||
|
||||
const video = claim && claim.value && claim.value.video;
|
||||
let duration;
|
||||
if (video && video.duration) {
|
||||
// $FlowFixMe
|
||||
let date = new Date(null);
|
||||
date.setSeconds(video.duration);
|
||||
let timeString = date.toISOString().substr(11, 8);
|
||||
|
||||
if (timeString.startsWith('00:')) {
|
||||
timeString = timeString.substr(3);
|
||||
}
|
||||
|
||||
duration = timeString;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-properties">
|
||||
{isSubscribed && <Icon tooltip icon={icons.SUBSCRIPTION} />}
|
||||
{!claimIsMine && downloaded && <Icon tooltip icon={icons.DOWNLOAD} />}
|
||||
{isRewardContent && <Icon tooltip icon={icons.FEATURED} />}
|
||||
<FilePrice hideFree uri={uri} />
|
||||
{duration && <span className="media__subtitle">{duration}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
// @flow
|
||||
import { remote } from 'electron';
|
||||
import React from 'react';
|
||||
import React, { Suspense } from 'react';
|
||||
import LoadingScreen from 'component/common/loading-screen';
|
||||
import VideoViewer from 'component/viewers/videoViewer';
|
||||
|
||||
// Audio player on hold until the current player is dropped
|
||||
// This component is half working
|
||||
// const AudioViewer = React.lazy<*>(() =>
|
||||
// import(
|
||||
// /* webpackChunkName: "audioViewer" */
|
||||
// 'component/viewers/audioViewer'
|
||||
// )
|
||||
// );
|
||||
// const AudioViewer = React.lazy<*>(() =>
|
||||
// import(/* webpackChunkName: "audioViewer" */
|
||||
// 'component/viewers/audioViewer')
|
||||
// );
|
||||
|
|
|
@ -56,7 +56,7 @@ export default function TagSelect(props: Props) {
|
|||
if (onSelect) {
|
||||
onSelect(tag);
|
||||
} else {
|
||||
doToggleTagFollow(tag);
|
||||
doToggleTagFollow(tag.name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,8 +14,10 @@ class TransactionListRecent extends React.PureComponent<Props> {
|
|||
componentDidMount() {
|
||||
const { fetchMyClaims, fetchTransactions } = this.props;
|
||||
|
||||
// @if TARGET='app'
|
||||
fetchMyClaims();
|
||||
fetchTransactions();
|
||||
// @endif
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -4,6 +4,7 @@ import Button from 'component/button';
|
|||
import { FormField } from 'component/common/form';
|
||||
import UserEmailNew from 'component/userEmailNew';
|
||||
import UserEmailVerify from 'component/userEmailVerify';
|
||||
import cookie from 'cookie';
|
||||
|
||||
type Props = {
|
||||
cancelButton: React.Node,
|
||||
|
@ -22,6 +23,15 @@ function UserEmail(props: Props) {
|
|||
isVerified = user.has_verified_email;
|
||||
}
|
||||
|
||||
const buttonsProps = IS_WEB
|
||||
? {
|
||||
onClick: () => {
|
||||
document.cookie = cookie.serialize('auth_token', '');
|
||||
window.location.reload();
|
||||
},
|
||||
}
|
||||
: { href: 'https://lbry.com/faq/how-to-change-email' };
|
||||
|
||||
return (
|
||||
<section className="card card--section">
|
||||
{!email && <UserEmailNew />}
|
||||
|
@ -43,9 +53,7 @@ function UserEmail(props: Props) {
|
|||
readOnly
|
||||
label={__('Your Email')}
|
||||
value={email}
|
||||
inputButton={
|
||||
<Button button="inverse" label={__('Change')} href="https://lbry.com/faq/how-to-change-email" />
|
||||
}
|
||||
inputButton={<Button button="inverse" label={__('Change')} {...buttonsProps} />}
|
||||
/>
|
||||
)}
|
||||
<p className="help">
|
||||
|
|
|
@ -15,9 +15,8 @@ export const SEND = 'send';
|
|||
export const SETTINGS = 'settings';
|
||||
export const SHOW = 'show';
|
||||
export const ACCOUNT = 'account';
|
||||
export const SUBSCRIPTIONS = 'subscriptions';
|
||||
export const FOLLOWING = 'following';
|
||||
export const SEARCH = 'search';
|
||||
export const TRANSACTIONS = 'transactions';
|
||||
export const TAGS = 'tags';
|
||||
export const WALLET = 'wallet';
|
||||
export const FOLLOWING = 'following';
|
||||
|
|
|
@ -9,6 +9,8 @@ import InvitePage from 'page/invite';
|
|||
|
||||
const WalletPage = () => (
|
||||
<Page>
|
||||
<UserEmail />
|
||||
|
||||
{IS_WEB && <UnsupportedOnWeb />}
|
||||
<div className={classnames({ 'card--disabled': IS_WEB })}>
|
||||
<div className="columns">
|
||||
|
|
|
@ -15,7 +15,7 @@ function DiscoverPage(props: Props) {
|
|||
<ClaimListDiscover
|
||||
personal
|
||||
tags={followedTags.map(tag => tag.name)}
|
||||
injectedItem={<TagsSelect showClose title={__('Make This Your Own')} />}
|
||||
injectedItem={<TagsSelect showClose title={__('Customize Your Homepage')} />}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
|
|
|
@ -184,6 +184,21 @@ class FilePage extends React.Component<Props> {
|
|||
|
||||
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance;
|
||||
|
||||
const video = claim && claim.value && claim.value.video;
|
||||
let duration;
|
||||
if (video && video.duration) {
|
||||
// $FlowFixMe
|
||||
let date = new Date(null);
|
||||
date.setSeconds(video.duration);
|
||||
let timeString = date.toISOString().substr(11, 8);
|
||||
|
||||
if (timeString.startsWith('00:')) {
|
||||
timeString = timeString.substr(3);
|
||||
}
|
||||
|
||||
duration = timeString;
|
||||
}
|
||||
|
||||
return (
|
||||
<Page className="main--file-page">
|
||||
<div className="grid-area--content card">
|
||||
|
@ -222,22 +237,11 @@ class FilePage extends React.Component<Props> {
|
|||
|
||||
<div className="grid-area--info media__content media__content--large">
|
||||
<h1 className="media__title media__title--large">{title}</h1>
|
||||
|
||||
<div className="media__actions media__actions--between">
|
||||
<div className="media__subtext media__subtext--large">
|
||||
<div className="media__subtitle__channel">
|
||||
<UriIndicator uri={uri} link />
|
||||
</div>
|
||||
{__('Published on')} <DateTime uri={uri} show={DateTime.SHOW_DATE} />
|
||||
<div className="media__subtext media__subtext--large">
|
||||
<div className="media__subtitle__channel">
|
||||
<UriIndicator uri={uri} link />
|
||||
</div>
|
||||
|
||||
{claimIsMine && (
|
||||
<p>
|
||||
{viewCount} {viewCount !== 1 ? __('Views') : __('View')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="media__actions media__actions--between">
|
||||
<div className="media__action-group--large">
|
||||
{claimIsMine && (
|
||||
|
@ -282,6 +286,25 @@ class FilePage extends React.Component<Props> {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="media__actions media__actions--between"
|
||||
style={{ marginTop: '1rem', paddingTop: '1rem', borderTop: '1px solid #ddd' }}
|
||||
>
|
||||
<div className="media__subtext media__subtext--large">
|
||||
<DateTime uri={uri} show={DateTime.SHOW_DATE} />
|
||||
</div>
|
||||
|
||||
<div className="media__subtext media__subtext--large">
|
||||
{video && <p className="media__info-text">{duration}</p>}
|
||||
|
||||
{claimIsMine && (
|
||||
<p>
|
||||
{viewCount} {viewCount !== 1 ? __('Views') : __('View')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="media__info--large">
|
||||
<ClaimTags uri={uri} type="large" />
|
||||
</div>
|
||||
|
|
|
@ -5,26 +5,22 @@ import TagsSelect from 'component/tagsSelect';
|
|||
import ClaimList from 'component/claimList';
|
||||
|
||||
type Props = {
|
||||
subscribedChannels: Array<{ uri: string }>,
|
||||
subscribedChannels: Array<Subscription>,
|
||||
};
|
||||
|
||||
function DiscoverPage(props: Props) {
|
||||
function FollowingEditPage(props: Props) {
|
||||
const { subscribedChannels } = props;
|
||||
|
||||
const channelUris = subscribedChannels.map(({ uri }) => uri);
|
||||
return (
|
||||
<Page>
|
||||
<div className="card">
|
||||
<TagsSelect showClose={false} title={__('Find New Tags To Follow')} />
|
||||
</div>
|
||||
<div className="card">
|
||||
<ClaimList
|
||||
header={<h1>{__('Channels You Are Following')}</h1>}
|
||||
empty={__("You aren't following any channels.")}
|
||||
uris={subscribedChannels.map(({ uri }) => uri)}
|
||||
/>
|
||||
<ClaimList uris={channelUris} />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default DiscoverPage;
|
||||
export default FollowingEditPage;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
// @flow
|
||||
import * as PAGES from 'constants/pages';
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Page from 'component/page';
|
||||
import ClaimList from 'component/claimList';
|
||||
import Button from 'component/button';
|
||||
|
@ -29,7 +29,7 @@ export default function SubscriptionsPage(props: Props) {
|
|||
doClaimSearch,
|
||||
uris,
|
||||
} = props;
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const hasSubscriptions = !!subscribedChannels.length;
|
||||
const { search } = location;
|
||||
const urlParams = new URLSearchParams(search);
|
||||
|
@ -53,27 +53,31 @@ export default function SubscriptionsPage(props: Props) {
|
|||
useEffect(() => {
|
||||
const ids = idString.split(',');
|
||||
const options = {
|
||||
page,
|
||||
channel_ids: ids,
|
||||
order_by: ['release_time'],
|
||||
};
|
||||
|
||||
doClaimSearch(20, options);
|
||||
}, [idString, doClaimSearch]);
|
||||
}, [idString, doClaimSearch, page]);
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<div className="card">
|
||||
<ClaimList
|
||||
loading={loading}
|
||||
header={<h1>{viewingSuggestedSubs ? __('Discover New Channels') : __('Latest From Your Subscriptions')}</h1>}
|
||||
header={
|
||||
<h1>{viewingSuggestedSubs ? __('Discover New Channels') : __("Latest From Who You're Following")}</h1>
|
||||
}
|
||||
headerAltControls={
|
||||
<Button
|
||||
button="link"
|
||||
label={viewingSuggestedSubs ? hasSubscriptions && __('Following') : __('Find New Channels')}
|
||||
label={viewingSuggestedSubs ? hasSubscriptions && __('View Your Feed') : __('Find New Channels')}
|
||||
onClick={() => onClick()}
|
||||
/>
|
||||
}
|
||||
uris={viewingSuggestedSubs ? suggestedSubscriptions.map(sub => sub.uri) : uris}
|
||||
onScrollBottom={() => console.log('scroll bottom') || setPage(page + 1)}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
|
|
|
@ -4,13 +4,17 @@ import WalletSend from 'component/walletSend';
|
|||
import WalletAddress from 'component/walletAddress';
|
||||
import TransactionListRecent from 'component/transactionListRecent';
|
||||
import Page from 'component/page';
|
||||
import UnsupportedOnWeb from 'component/common/unsupported-on-web';
|
||||
|
||||
const WalletPage = () => (
|
||||
<Page>
|
||||
<WalletBalance />
|
||||
<TransactionListRecent />
|
||||
<WalletSend />
|
||||
<WalletAddress />
|
||||
{IS_WEB && <UnsupportedOnWeb />}
|
||||
<div className={IS_WEB && 'card--disabled'}>
|
||||
<WalletBalance />
|
||||
<TransactionListRecent />
|
||||
<WalletSend />
|
||||
<WalletAddress />
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
|
||||
|
|
|
@ -13,13 +13,13 @@
|
|||
@import 'component/button';
|
||||
@import 'component/card';
|
||||
@import 'component/channel';
|
||||
@import 'component/claim-list';
|
||||
@import 'component/comments';
|
||||
@import 'component/content';
|
||||
@import 'component/credit';
|
||||
@import 'component/dat-gui';
|
||||
@import 'component/expandable';
|
||||
@import 'component/file-download';
|
||||
@import 'component/file-list';
|
||||
@import 'component/file-properties';
|
||||
@import 'component/file-render';
|
||||
@import 'component/form-field';
|
||||
|
|
|
@ -16,7 +16,7 @@ $metadata-z-index: 1;
|
|||
align-self: flex-start;
|
||||
position: absolute;
|
||||
object-fit: cover;
|
||||
filter: brightness(60%);
|
||||
filter: brightness(50%);
|
||||
}
|
||||
|
||||
.channel-cover,
|
||||
|
@ -27,8 +27,8 @@ $metadata-z-index: 1;
|
|||
|
||||
.channel-thumbnail {
|
||||
display: flex;
|
||||
height: 5.3rem;
|
||||
width: 5.4rem;
|
||||
height: 5rem;
|
||||
width: 6rem;
|
||||
background-size: cover;
|
||||
margin-right: var(--spacing-medium);
|
||||
}
|
||||
|
@ -52,7 +52,6 @@ $metadata-z-index: 1;
|
|||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
align-self: flex-end;
|
||||
// margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.channel-thumbnail,
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
background-size: 1.2rem;
|
||||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg' fill='%23ffffff'%3E%3Cpath d='M17.172, 31.172c1.562, -1.562 4.095, -1.562 5.656, 0l25.172, 25.171l25.172, -25.171c1.562, -1.562 4.095, -1.562 5.656, 0c1.562, 1.562 1.562, 4.095 0, 5.656l-28, 28c-1.562, 1.562 -4.095, 1.562 -5.656, 0l-28, -28c-0.781, -0.781 -1.172, -1.805 -1.172, -2.828c0, -1.023 0.391, -2.047 1.172, -2.828Z'/%3E%3C/svg%3E%0A");
|
||||
height: 2.5rem;
|
||||
font-size: 1.3rem;
|
||||
padding: 0 var(--spacing-medium);
|
||||
padding-right: var(--spacing-large);
|
||||
margin-bottom: 0;
|
||||
|
@ -60,16 +61,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.claim-list__header-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.claim-list__header-text,
|
||||
.claim-list__dropdown {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.claim-list__alt-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -81,7 +72,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.claim-list__item {
|
||||
.claim-preview {
|
||||
display: flex;
|
||||
position: relative;
|
||||
font-size: 1.3rem;
|
||||
|
@ -104,12 +95,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.claim-list__item--injected,
|
||||
.claim-list__item + .claim-list__item {
|
||||
.claim-preview--injected,
|
||||
.claim-preview {
|
||||
border-top: 1px solid rgba($lbry-teal-5, 0.1);
|
||||
}
|
||||
|
||||
.claim-list__item--large {
|
||||
.claim-preview--large {
|
||||
@include mediaThumbHoverZoom;
|
||||
font-size: 1.6rem;
|
||||
border-bottom: 0;
|
||||
|
@ -138,32 +129,32 @@
|
|||
}
|
||||
}
|
||||
|
||||
.claim-list__item-metadata {
|
||||
.claim-preview-metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.claim-list__item-info {
|
||||
.claim-preview-info {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.claim-list__item-info,
|
||||
.claim-list__item-properties {
|
||||
.claim-preview-info,
|
||||
.claim-preview-properties {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.claim-list__item-properties {
|
||||
.claim-preview-properties {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.claim-list__item-title {
|
||||
.claim-preview-title {
|
||||
font-weight: 600;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.claim-list__item-tags {
|
||||
.claim-preview-tags {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
|
@ -3,6 +3,14 @@
|
|||
position: relative;
|
||||
align-items: center;
|
||||
|
||||
.icon {
|
||||
stroke: rgba($lbry-black, 0.5);
|
||||
|
||||
html[data-mode='dark'] & {
|
||||
stroke: rgba($lbry-white, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
& > *:not(:last-child) {
|
||||
margin-right: var(--spacing-small);
|
||||
}
|
||||
|
|
|
@ -132,10 +132,6 @@
|
|||
color: rgba($lbry-black, 0.8);
|
||||
font-size: 0.9em;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--spacing-medium);
|
||||
}
|
||||
|
||||
html[data-mode='dark'] & {
|
||||
color: rgba($lbry-white, 0.7);
|
||||
}
|
||||
|
@ -150,7 +146,7 @@
|
|||
|
||||
.media__subtitle {
|
||||
font-size: 0.8em;
|
||||
color: rgba($lbry-black, 0.8);
|
||||
color: rgba($lbry-black, 0.6);
|
||||
|
||||
[data-mode='dark'] & {
|
||||
color: rgba($lbry-white, 0.8);
|
||||
|
@ -167,6 +163,7 @@
|
|||
|
||||
.media__subtitle__channel {
|
||||
font-weight: 600;
|
||||
margin: var(--spacing-small) 0;
|
||||
}
|
||||
|
||||
// M E D I A
|
||||
|
@ -181,7 +178,7 @@
|
|||
}
|
||||
|
||||
.media__info--large {
|
||||
border-top: 1px solid $lbry-gray-1;
|
||||
// border-top: 1px solid $lbry-gray-1;
|
||||
margin-top: var(--spacing-medium);
|
||||
|
||||
html[data-mode='dark'] & {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
.placeholder {
|
||||
display: flex;
|
||||
|
||||
&.claim-list__item-title {
|
||||
&.claim-preview-title {
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
}
|
||||
|
|
|
@ -90,7 +90,6 @@
|
|||
|
||||
.icon {
|
||||
margin-right: var(--spacing-small);
|
||||
margin-bottom: 0.2rem;
|
||||
stroke: $lbry-gray-5;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
@mixin placeholder {
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
background-color: $lbry-gray-2;
|
||||
background-color: $lbry-gray-1;
|
||||
border-radius: var(--card-radius);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<meta property="og:description" content="All your favorite LBRY content in your browser." />
|
||||
<meta property="og:image" content="/og.png" />
|
||||
<!-- @endif -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<!-- <meta name="viewport" content="width=device-width, initial-scale=1" /> -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -6641,9 +6641,9 @@ lazy-val@^1.0.3, lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#b3bf3f6d53410ff1c5415b51ca425341e364959f:
|
||||
lbry-redux@lbryio/lbry-redux#2930ad82a90ca91f6caf3761597ef9a67da7db66:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/b3bf3f6d53410ff1c5415b51ca425341e364959f"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/2930ad82a90ca91f6caf3761597ef9a67da7db66"
|
||||
dependencies:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
|
|
Loading…
Reference in a new issue
@kauffj does this seem like a reasonable way to do this or is there something simpler?
It is to check if a full page was loaded, if 15 items are loaded don't fetch more, because page 2 wont' exist. If that seems fine I can create a prop called
claimListSearchCount
or something and add a constant that we use for theclaim_search
params