first version of infinite scroll

This commit is contained in:
Sean Yesmunt 2019-06-27 02:18:45 -04:00
parent 68a18556bf
commit 2ca254a573
33 changed files with 236 additions and 110 deletions

View file

@ -1,6 +1,6 @@
{ {
"name": "LBRY", "name": "LBRY",
"version": "0.33.1", "version": "0.33.2",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.", "description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"keywords": [ "keywords": [
"lbry" "lbry"
@ -123,7 +123,7 @@
"jsmediatags": "^3.8.1", "jsmediatags": "^3.8.1",
"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#141593500693a93db74c62ef5a9fe67b43896603", "lbry-redux": "lbryio/lbry-redux#2930ad82a90ca91f6caf3761597ef9a67da7db66",
"lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845", "lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -1,6 +1,6 @@
import { hot } from 'react-hot-loader/root'; import { hot } from 'react-hot-loader/root';
import { connect } from 'react-redux'; 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 { selectUser, doRewardList, doFetchRewardedContent } from 'lbryinc';
import { selectThemePath } from 'redux/selectors/settings'; import { selectThemePath } from 'redux/selectors/settings';
import App from './view'; import App from './view';
@ -15,6 +15,7 @@ const perform = dispatch => ({
updateBlockHeight: () => dispatch(doUpdateBlockHeight()), updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
fetchRewards: () => dispatch(doRewardList()), fetchRewards: () => dispatch(doRewardList()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()), fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchTransactions: () => dispatch(doFetchTransactions()),
}); });
export default hot( export default hot(

View file

@ -9,6 +9,8 @@ import { openContextMenu } from 'util/context-menu';
import useKonamiListener from 'util/enhanced-layout'; import useKonamiListener from 'util/enhanced-layout';
import Yrbl from 'component/yrbl'; import Yrbl from 'component/yrbl';
export const MAIN_WRAPPER_CLASS = 'main-wrapper';
type Props = { type Props = {
alertError: (string | {}) => void, alertError: (string | {}) => void,
pageTitle: ?string, pageTitle: ?string,
@ -16,18 +18,23 @@ type Props = {
theme: string, theme: string,
fetchRewards: () => void, fetchRewards: () => void,
fetchRewardedContent: () => void, fetchRewardedContent: () => void,
fetchTransactions: () => void,
}; };
function App(props: Props) { function App(props: Props) {
const { theme, fetchRewards, fetchRewardedContent } = props; const { theme, fetchRewards, fetchRewardedContent, fetchTransactions } = props;
const appRef = useRef(); const appRef = useRef();
const isEnhancedLayout = useKonamiListener(); const isEnhancedLayout = useKonamiListener();
useEffect(() => { useEffect(() => {
ReactModal.setAppElement(appRef.current); ReactModal.setAppElement(appRef.current);
fetchRewards();
fetchRewardedContent(); fetchRewardedContent();
}, [fetchRewards, fetchRewardedContent]);
// @if TARGET='app'
fetchRewards();
fetchTransactions();
// @endif
}, [fetchRewards, fetchRewardedContent, fetchTransactions]);
useEffect(() => { useEffect(() => {
// $FlowFixMe // $FlowFixMe
@ -38,7 +45,7 @@ function App(props: Props) {
<div ref={appRef} onContextMenu={e => openContextMenu(e)}> <div ref={appRef} onContextMenu={e => openContextMenu(e)}>
<Header /> <Header />
<div className="main-wrapper"> <div className={MAIN_WRAPPER_CLASS}>
<div className="main-wrapper-inner"> <div className="main-wrapper-inner">
<Router /> <Router />
<SideBar /> <SideBar />

View file

@ -35,7 +35,7 @@ function ChannelContent(props: Props) {
{!channelIsMine && <HiddenNsfwClaims className="card__content help" uri={uri} />} {!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 <Paginate
onPageChange={page => fetchClaims(uri, page)} onPageChange={page => fetchClaims(uri, page)}

View file

@ -1,6 +1,7 @@
// @flow // @flow
import { MAIN_WRAPPER_CLASS } from 'component/app/view';
import type { Node } from 'react'; import type { Node } from 'react';
import React from 'react'; import React, { useEffect } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
@ -19,14 +20,25 @@ type Props = {
type: string, type: string,
empty?: string, empty?: string,
meta?: Node, meta?: Node,
onScrollBottom?: any => void,
// 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,
}; };
export default function ClaimList(props: Props) { 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 [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
const sortedUris = uris && currentSort === SORT_NEW ? uris.slice().reverse() : uris;
const hasUris = uris && !!uris.length; const hasUris = uris && !!uris.length;
const sortedUris = (hasUris && (currentSort === SORT_NEW ? uris : uris.slice().reverse())) || []; 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); setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
} }
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 ( return (
<section className={classnames('file-list')}> <section className={classnames('claim-list')}>
{header !== false && ( {header !== false && (
<div className={classnames('claim-list__header', { 'claim-list__header--small': type === 'small' })}> <div className={classnames('claim-list__header', { 'claim-list__header--small': type === 'small' })}>
{header || ( {header || (
@ -60,7 +95,7 @@ export default function ClaimList(props: Props) {
{sortedUris.map((uri, index) => ( {sortedUris.map((uri, index) => (
<React.Fragment key={uri}> <React.Fragment key={uri}>
<ClaimPreview uri={uri} type={type} /> <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> </React.Fragment>
))} ))}
</ul> </ul>

View file

@ -1,12 +1,14 @@
// @flow // @flow
import type { Node } from 'react'; import type { Node } from 'react';
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import moment from 'moment'; import moment from 'moment';
import usePersistedState from 'util/use-persisted-state';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import Tag from 'component/tag'; 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_DAY = 'day';
const TIME_WEEK = 'week'; const TIME_WEEK = 'week';
const TIME_MONTH = 'month'; const TIME_MONTH = 'month';
@ -37,11 +39,17 @@ function ClaimListDiscover(props: Props) {
const [personalSort, setPersonalSort] = usePersistedState('file-list-trending:personalSort', SEARCH_SORT_YOU); const [personalSort, setPersonalSort] = usePersistedState('file-list-trending:personalSort', SEARCH_SORT_YOU);
const [typeSort, setTypeSort] = usePersistedState('file-list-trending:typeSort', TYPE_TRENDING); const [typeSort, setTypeSort] = usePersistedState('file-list-trending:typeSort', TYPE_TRENDING);
const [timeSort, setTimeSort] = usePersistedState('file-list-trending:timeSort', TIME_WEEK); 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 toCapitalCase = string => string.charAt(0).toUpperCase() + string.slice(1);
const tagsString = tags.join(','); const tagsString = tags.join(',');
useEffect(() => { 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(','); const newTags = tagsString.split(',');
if ((newTags && !personal) || (newTags && personal && personalSort === SEARCH_SORT_YOU)) { if ((newTags && !personal) || (newTags && personal && personalSort === SEARCH_SORT_YOU)) {
@ -65,7 +73,7 @@ function ClaimListDiscover(props: Props) {
} }
doClaimSearch(20, options); doClaimSearch(20, options);
}, [personal, personalSort, typeSort, timeSort, doClaimSearch, tagsString]); }, [personal, personalSort, typeSort, timeSort, doClaimSearch, page, tagsString]);
const header = ( const header = (
<h1 className="card__title--flex"> <h1 className="card__title--flex">
@ -91,7 +99,10 @@ function ClaimListDiscover(props: Props) {
name="trending_overview" name="trending_overview"
className="claim-list__dropdown" className="claim-list__dropdown"
value={personalSort} value={personalSort}
onChange={e => setPersonalSort(e.target.value)} onChange={e => {
setPage(1);
setPersonalSort(e.target.value);
}}
> >
{SEARCH_FILTER_TYPES.map(type => ( {SEARCH_FILTER_TYPES.map(type => (
<option key={type} value={type}> <option key={type} value={type}>
@ -132,7 +143,10 @@ function ClaimListDiscover(props: Props) {
injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem} injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem}
header={header} header={header}
headerAltControls={headerAltControls} headerAltControls={headerAltControls}
onScrollBottom={() => setPage(page + 1)}
/> />
{loading && page > 1 && new Array(PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder />)}
</div> </div>
); );
} }

View file

@ -1,7 +1,7 @@
// @flow // @flow
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { convertToShareLink } from 'lbry-redux'; import { parseURI, convertToShareLink } from 'lbry-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { openCopyLinkMenu } from 'util/context-menu'; import { openCopyLinkMenu } from 'util/context-menu';
import { formatLbryUriForWeb } from 'util/uri'; import { formatLbryUriForWeb } from 'util/uri';
@ -53,8 +53,8 @@ function ClaimPreview(props: Props) {
blackListedOutpoints, blackListedOutpoints,
} = props; } = props;
const haventFetched = claim === undefined; const haventFetched = claim === undefined;
const abandoned = !isResolvingUri && !claim; const abandoned = !isResolvingUri && !claim && !placeholder;
const isChannel = claim && claim.value_type === 'channel'; const { isChannel } = parseURI(uri);
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0; const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
let shouldHide = abandoned || (!claimIsMine && obscureNsfw && nsfw); let shouldHide = abandoned || (!claimIsMine && obscureNsfw && nsfw);
@ -94,10 +94,10 @@ function ClaimPreview(props: Props) {
if (placeholder && !claim) { if (placeholder && !claim) {
return ( return (
<li className="claim-list__item" disabled> <li className="claim-preview" disabled>
<div className="placeholder media__thumb" /> <div className="placeholder media__thumb" />
<div className="placeholder__wrapper"> <div className="placeholder__wrapper">
<div className="placeholder claim-list__item-title" /> <div className="placeholder claim-preview-title" />
<div className="placeholder media__subtitle" /> <div className="placeholder media__subtitle" />
</div> </div>
</li> </li>
@ -109,15 +109,15 @@ function ClaimPreview(props: Props) {
role="link" role="link"
onClick={pending ? undefined : onClick} onClick={pending ? undefined : onClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
className={classnames('claim-list__item', { className={classnames('claim-preview', {
'claim-list__item--large': type === 'large', 'claim-preview--large': type === 'large',
'claim-list__pending': pending, 'claim-list__pending': pending,
})} })}
> >
{isChannel ? <ChannelThumbnail uri={uri} /> : <CardMedia thumbnail={thumbnail} />} {isChannel ? <ChannelThumbnail uri={uri} /> : <CardMedia thumbnail={thumbnail} />}
<div className="claim-list__item-metadata"> <div className="claim-preview-metadata">
<div className="claim-list__item-info"> <div className="claim-preview-info">
<div className="claim-list__item-title"> <div className="claim-preview-title">
<TruncatedText text={title || (claim && claim.name)} lines={1} /> <TruncatedText text={title || (claim && claim.name)} lines={1} />
</div> </div>
{type !== 'small' && ( {type !== 'small' && (
@ -128,7 +128,7 @@ function ClaimPreview(props: Props) {
)} )}
</div> </div>
<div className="claim-list__item-properties"> <div className="claim-preview-properties">
<div className="media__subtitle"> <div className="media__subtitle">
<UriIndicator uri={uri} link /> <UriIndicator uri={uri} link />
{pending && <div>Pending...</div>} {pending && <div>Pending...</div>}

View file

@ -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" /> <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> </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( [ICONS.ARROW_LEFT]: buildIcon(
<g fill="none" fillRule="evenodd" strokeLinecap="round"> <g fill="none" fillRule="evenodd" strokeLinecap="round">
<path d="M4, 12 L21, 12" /> <path d="M4, 12 L21, 12" />
@ -210,4 +216,12 @@ export const icons = {
[ICONS.CHAT]: buildIcon( [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" /> <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" />),
}; };

View file

@ -42,7 +42,6 @@ class FileDetails extends PureComponent<Props> {
<Expandable> <Expandable>
{description && ( {description && (
<Fragment> <Fragment>
<div className="media__info-title">About</div>
<div className="media__info-text"> <div className="media__info-text">
<MarkdownPreview content={description} promptLinks /> <MarkdownPreview content={description} promptLinks />
</div> </div>

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectFileInfoForUri, makeSelectClaimIsMine } from 'lbry-redux'; import { makeSelectFileInfoForUri, makeSelectClaimIsMine, makeSelectClaimForUri } from 'lbry-redux';
import { selectRewardContentClaimIds } from 'lbryinc'; import { selectRewardContentClaimIds } from 'lbryinc';
import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed, makeSelectIsNew } from 'redux/selectors/subscriptions';
import FileProperties from './view'; import FileProperties from './view';
@ -10,6 +10,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),
claim: makeSelectClaimForUri(props.uri)(state),
}); });
export default connect( export default connect(

View file

@ -7,6 +7,7 @@ import FilePrice from 'component/filePrice';
type Props = { type Props = {
uri: string, uri: string,
claim: ?StreamClaim,
downloaded: boolean, downloaded: boolean,
claimIsMine: boolean, claimIsMine: boolean,
isSubscribed: boolean, isSubscribed: boolean,
@ -15,16 +16,32 @@ type Props = {
}; };
export default function FileProperties(props: 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 { claimId } = parseURI(uri);
const isRewardContent = rewardedContentClaimIds.includes(claimId); 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 ( return (
<div className="file-properties"> <div className="file-properties">
{isSubscribed && <Icon tooltip icon={icons.SUBSCRIPTION} />} {isSubscribed && <Icon tooltip icon={icons.SUBSCRIPTION} />}
{!claimIsMine && downloaded && <Icon tooltip icon={icons.DOWNLOAD} />} {!claimIsMine && downloaded && <Icon tooltip icon={icons.DOWNLOAD} />}
{isRewardContent && <Icon tooltip icon={icons.FEATURED} />} {isRewardContent && <Icon tooltip icon={icons.FEATURED} />}
<FilePrice hideFree uri={uri} /> <FilePrice hideFree uri={uri} />
{duration && <span className="media__subtitle">{duration}</span>}
</div> </div>
); );
} }

View file

@ -1,12 +1,18 @@
// @flow // @flow
import { remote } from 'electron'; import { remote } from 'electron';
import React from 'react'; import React, { Suspense } from 'react';
import LoadingScreen from 'component/common/loading-screen'; import LoadingScreen from 'component/common/loading-screen';
import VideoViewer from 'component/viewers/videoViewer'; import VideoViewer from 'component/viewers/videoViewer';
// Audio player on hold until the current player is dropped // Audio player on hold until the current player is dropped
// This component is half working // This component is half working
// const AudioViewer = React.lazy<*>(() => // const AudioViewer = React.lazy<*>(() =>
// import(
// /* webpackChunkName: "audioViewer" */
// 'component/viewers/audioViewer'
// )
// );
// const AudioViewer = React.lazy<*>(() =>
// import(/* webpackChunkName: "audioViewer" */ // import(/* webpackChunkName: "audioViewer" */
// 'component/viewers/audioViewer') // 'component/viewers/audioViewer')
// ); // );

View file

@ -56,7 +56,7 @@ export default function TagSelect(props: Props) {
if (onSelect) { if (onSelect) {
onSelect(tag); onSelect(tag);
} else { } else {
doToggleTagFollow(tag); doToggleTagFollow(tag.name);
} }
} }

View file

@ -14,8 +14,10 @@ class TransactionListRecent extends React.PureComponent<Props> {
componentDidMount() { componentDidMount() {
const { fetchMyClaims, fetchTransactions } = this.props; const { fetchMyClaims, fetchTransactions } = this.props;
// @if TARGET='app'
fetchMyClaims(); fetchMyClaims();
fetchTransactions(); fetchTransactions();
// @endif
} }
render() { render() {

View file

@ -4,6 +4,7 @@ import Button from 'component/button';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import UserEmailNew from 'component/userEmailNew'; import UserEmailNew from 'component/userEmailNew';
import UserEmailVerify from 'component/userEmailVerify'; import UserEmailVerify from 'component/userEmailVerify';
import cookie from 'cookie';
type Props = { type Props = {
cancelButton: React.Node, cancelButton: React.Node,
@ -22,6 +23,15 @@ function UserEmail(props: Props) {
isVerified = user.has_verified_email; 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 ( return (
<section className="card card--section"> <section className="card card--section">
{!email && <UserEmailNew />} {!email && <UserEmailNew />}
@ -43,9 +53,7 @@ function UserEmail(props: Props) {
readOnly readOnly
label={__('Your Email')} label={__('Your Email')}
value={email} value={email}
inputButton={ inputButton={<Button button="inverse" label={__('Change')} {...buttonsProps} />}
<Button button="inverse" label={__('Change')} href="https://lbry.com/faq/how-to-change-email" />
}
/> />
)} )}
<p className="help"> <p className="help">

View file

@ -15,9 +15,8 @@ export const SEND = 'send';
export const SETTINGS = 'settings'; export const SETTINGS = 'settings';
export const SHOW = 'show'; export const SHOW = 'show';
export const ACCOUNT = 'account'; export const ACCOUNT = 'account';
export const SUBSCRIPTIONS = 'subscriptions'; export const FOLLOWING = 'following';
export const SEARCH = 'search'; export const SEARCH = 'search';
export const TRANSACTIONS = 'transactions'; export const TRANSACTIONS = 'transactions';
export const TAGS = 'tags'; export const TAGS = 'tags';
export const WALLET = 'wallet'; export const WALLET = 'wallet';
export const FOLLOWING = 'following';

View file

@ -9,6 +9,8 @@ import InvitePage from 'page/invite';
const WalletPage = () => ( const WalletPage = () => (
<Page> <Page>
<UserEmail />
{IS_WEB && <UnsupportedOnWeb />} {IS_WEB && <UnsupportedOnWeb />}
<div className={classnames({ 'card--disabled': IS_WEB })}> <div className={classnames({ 'card--disabled': IS_WEB })}>
<div className="columns"> <div className="columns">

View file

@ -15,7 +15,7 @@ function DiscoverPage(props: Props) {
<ClaimListDiscover <ClaimListDiscover
personal personal
tags={followedTags.map(tag => tag.name)} tags={followedTags.map(tag => tag.name)}
injectedItem={<TagsSelect showClose title={__('Make This Your Own')} />} injectedItem={<TagsSelect showClose title={__('Customize Your Homepage')} />}
/> />
</Page> </Page>
); );

View file

@ -184,6 +184,21 @@ class FilePage extends React.Component<Props> {
const insufficientCredits = !claimIsMine && costInfo && costInfo.cost > balance; 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 ( return (
<Page className="main--file-page"> <Page className="main--file-page">
<div className="grid-area--content card"> <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"> <div className="grid-area--info media__content media__content--large">
<h1 className="media__title media__title--large">{title}</h1> <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__subtext media__subtext--large">
<div className="media__subtitle__channel"> <div className="media__subtitle__channel">
<UriIndicator uri={uri} link /> <UriIndicator uri={uri} link />
</div> </div>
{__('Published on')} <DateTime uri={uri} show={DateTime.SHOW_DATE} />
</div> </div>
{claimIsMine && (
<p>
{viewCount} {viewCount !== 1 ? __('Views') : __('View')}
</p>
)}
</div>
<div className="media__actions media__actions--between"> <div className="media__actions media__actions--between">
<div className="media__action-group--large"> <div className="media__action-group--large">
{claimIsMine && ( {claimIsMine && (
@ -282,6 +286,25 @@ class FilePage extends React.Component<Props> {
</div> </div>
</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"> <div className="media__info--large">
<ClaimTags uri={uri} type="large" /> <ClaimTags uri={uri} type="large" />
</div> </div>

View file

@ -5,26 +5,22 @@ import TagsSelect from 'component/tagsSelect';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
type Props = { type Props = {
subscribedChannels: Array<{ uri: string }>, subscribedChannels: Array<Subscription>,
}; };
function DiscoverPage(props: Props) { function FollowingEditPage(props: Props) {
const { subscribedChannels } = props; const { subscribedChannels } = props;
const channelUris = subscribedChannels.map(({ uri }) => uri);
return ( return (
<Page> <Page>
<div className="card"> <div className="card">
<TagsSelect showClose={false} title={__('Find New Tags To Follow')} /> <TagsSelect showClose={false} title={__('Find New Tags To Follow')} />
</div> </div>
<div className="card"> <div className="card">
<ClaimList <ClaimList uris={channelUris} />
header={<h1>{__('Channels You Are Following')}</h1>}
empty={__("You aren't following any channels.")}
uris={subscribedChannels.map(({ uri }) => uri)}
/>
</div> </div>
</Page> </Page>
); );
} }
export default DiscoverPage; export default FollowingEditPage;

View file

@ -1,6 +1,6 @@
// @flow // @flow
import * as PAGES from 'constants/pages'; import * as PAGES from 'constants/pages';
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import Page from 'component/page'; import Page from 'component/page';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import Button from 'component/button'; import Button from 'component/button';
@ -29,7 +29,7 @@ export default function SubscriptionsPage(props: Props) {
doClaimSearch, doClaimSearch,
uris, uris,
} = props; } = props;
const [page, setPage] = useState(1);
const hasSubscriptions = !!subscribedChannels.length; const hasSubscriptions = !!subscribedChannels.length;
const { search } = location; const { search } = location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
@ -53,27 +53,31 @@ export default function SubscriptionsPage(props: Props) {
useEffect(() => { useEffect(() => {
const ids = idString.split(','); const ids = idString.split(',');
const options = { const options = {
page,
channel_ids: ids, channel_ids: ids,
order_by: ['release_time'], order_by: ['release_time'],
}; };
doClaimSearch(20, options); doClaimSearch(20, options);
}, [idString, doClaimSearch]); }, [idString, doClaimSearch, page]);
return ( return (
<Page> <Page>
<div className="card"> <div className="card">
<ClaimList <ClaimList
loading={loading} 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={ headerAltControls={
<Button <Button
button="link" button="link"
label={viewingSuggestedSubs ? hasSubscriptions && __('Following') : __('Find New Channels')} label={viewingSuggestedSubs ? hasSubscriptions && __('View Your Feed') : __('Find New Channels')}
onClick={() => onClick()} onClick={() => onClick()}
/> />
} }
uris={viewingSuggestedSubs ? suggestedSubscriptions.map(sub => sub.uri) : uris} uris={viewingSuggestedSubs ? suggestedSubscriptions.map(sub => sub.uri) : uris}
onScrollBottom={() => console.log('scroll bottom') || setPage(page + 1)}
/> />
</div> </div>
</Page> </Page>

View file

@ -4,13 +4,17 @@ import WalletSend from 'component/walletSend';
import WalletAddress from 'component/walletAddress'; import WalletAddress from 'component/walletAddress';
import TransactionListRecent from 'component/transactionListRecent'; import TransactionListRecent from 'component/transactionListRecent';
import Page from 'component/page'; import Page from 'component/page';
import UnsupportedOnWeb from 'component/common/unsupported-on-web';
const WalletPage = () => ( const WalletPage = () => (
<Page> <Page>
{IS_WEB && <UnsupportedOnWeb />}
<div className={IS_WEB && 'card--disabled'}>
<WalletBalance /> <WalletBalance />
<TransactionListRecent /> <TransactionListRecent />
<WalletSend /> <WalletSend />
<WalletAddress /> <WalletAddress />
</div>
</Page> </Page>
); );

View file

@ -13,13 +13,13 @@
@import 'component/button'; @import 'component/button';
@import 'component/card'; @import 'component/card';
@import 'component/channel'; @import 'component/channel';
@import 'component/claim-list';
@import 'component/comments'; @import 'component/comments';
@import 'component/content'; @import 'component/content';
@import 'component/credit'; @import 'component/credit';
@import 'component/dat-gui'; @import 'component/dat-gui';
@import 'component/expandable'; @import 'component/expandable';
@import 'component/file-download'; @import 'component/file-download';
@import 'component/file-list';
@import 'component/file-properties'; @import 'component/file-properties';
@import 'component/file-render'; @import 'component/file-render';
@import 'component/form-field'; @import 'component/form-field';

View file

@ -16,7 +16,7 @@ $metadata-z-index: 1;
align-self: flex-start; align-self: flex-start;
position: absolute; position: absolute;
object-fit: cover; object-fit: cover;
filter: brightness(60%); filter: brightness(50%);
} }
.channel-cover, .channel-cover,
@ -27,8 +27,8 @@ $metadata-z-index: 1;
.channel-thumbnail { .channel-thumbnail {
display: flex; display: flex;
height: 5.3rem; height: 5rem;
width: 5.4rem; width: 6rem;
background-size: cover; background-size: cover;
margin-right: var(--spacing-medium); margin-right: var(--spacing-medium);
} }
@ -52,7 +52,6 @@ $metadata-z-index: 1;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
align-self: flex-end; align-self: flex-end;
// margin-bottom: -1px;
} }
.channel-thumbnail, .channel-thumbnail,

View file

@ -43,6 +43,7 @@
background-size: 1.2rem; 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"); 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; height: 2.5rem;
font-size: 1.3rem;
padding: 0 var(--spacing-medium); padding: 0 var(--spacing-medium);
padding-right: var(--spacing-large); padding-right: var(--spacing-large);
margin-bottom: 0; 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 { .claim-list__alt-controls {
display: flex; display: flex;
align-items: center; align-items: center;
@ -81,7 +72,7 @@
} }
} }
.claim-list__item { .claim-preview {
display: flex; display: flex;
position: relative; position: relative;
font-size: 1.3rem; font-size: 1.3rem;
@ -104,12 +95,12 @@
} }
} }
.claim-list__item--injected, .claim-preview--injected,
.claim-list__item + .claim-list__item { .claim-preview {
border-top: 1px solid rgba($lbry-teal-5, 0.1); border-top: 1px solid rgba($lbry-teal-5, 0.1);
} }
.claim-list__item--large { .claim-preview--large {
@include mediaThumbHoverZoom; @include mediaThumbHoverZoom;
font-size: 1.6rem; font-size: 1.6rem;
border-bottom: 0; border-bottom: 0;
@ -138,32 +129,32 @@
} }
} }
.claim-list__item-metadata { .claim-preview-metadata {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
} }
.claim-list__item-info { .claim-preview-info {
align-items: flex-start; align-items: flex-start;
} }
.claim-list__item-info, .claim-preview-info,
.claim-list__item-properties { .claim-preview-properties {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.claim-list__item-properties { .claim-preview-properties {
align-items: flex-end; align-items: flex-end;
} }
.claim-list__item-title { .claim-preview-title {
font-weight: 600; font-weight: 600;
margin-right: auto; margin-right: auto;
} }
.claim-list__item-tags { .claim-preview-tags {
margin-left: 0; margin-left: 0;
} }

View file

@ -3,6 +3,14 @@
position: relative; position: relative;
align-items: center; align-items: center;
.icon {
stroke: rgba($lbry-black, 0.5);
html[data-mode='dark'] & {
stroke: rgba($lbry-white, 0.7);
}
}
& > *:not(:last-child) { & > *:not(:last-child) {
margin-right: var(--spacing-small); margin-right: var(--spacing-small);
} }

View file

@ -132,10 +132,6 @@
color: rgba($lbry-black, 0.8); color: rgba($lbry-black, 0.8);
font-size: 0.9em; font-size: 0.9em;
&:not(:last-child) {
margin-bottom: var(--spacing-medium);
}
html[data-mode='dark'] & { html[data-mode='dark'] & {
color: rgba($lbry-white, 0.7); color: rgba($lbry-white, 0.7);
} }
@ -150,7 +146,7 @@
.media__subtitle { .media__subtitle {
font-size: 0.8em; font-size: 0.8em;
color: rgba($lbry-black, 0.8); color: rgba($lbry-black, 0.6);
[data-mode='dark'] & { [data-mode='dark'] & {
color: rgba($lbry-white, 0.8); color: rgba($lbry-white, 0.8);
@ -167,6 +163,7 @@
.media__subtitle__channel { .media__subtitle__channel {
font-weight: 600; font-weight: 600;
margin: var(--spacing-small) 0;
} }
// M E D I A // M E D I A
@ -181,7 +178,7 @@
} }
.media__info--large { .media__info--large {
border-top: 1px solid $lbry-gray-1; // border-top: 1px solid $lbry-gray-1;
margin-top: var(--spacing-medium); margin-top: var(--spacing-medium);
html[data-mode='dark'] & { html[data-mode='dark'] & {

View file

@ -9,7 +9,7 @@
.placeholder { .placeholder {
display: flex; display: flex;
&.claim-list__item-title { &.claim-preview-title {
width: 100%; width: 100%;
height: 3rem; height: 3rem;
} }

View file

@ -90,7 +90,6 @@
.icon { .icon {
margin-right: var(--spacing-small); margin-right: var(--spacing-small);
margin-bottom: 0.2rem;
stroke: $lbry-gray-5; stroke: $lbry-gray-5;
} }

View file

@ -1,6 +1,6 @@
@mixin placeholder { @mixin placeholder {
animation: pulse 2s infinite ease-in-out; animation: pulse 2s infinite ease-in-out;
background-color: $lbry-gray-2; background-color: $lbry-gray-1;
border-radius: var(--card-radius); border-radius: var(--card-radius);
} }

View file

@ -12,7 +12,7 @@
<meta property="og:description" content="All your favorite LBRY content in your browser." /> <meta property="og:description" content="All your favorite LBRY content in your browser." />
<meta property="og:image" content="/og.png" /> <meta property="og:image" content="/og.png" />
<!-- @endif --> <!-- @endif -->
<meta name="viewport" content="width=device-width, initial-scale=1" /> <!-- <meta name="viewport" content="width=device-width, initial-scale=1" /> -->
</head> </head>
<body> <body>

View file

@ -6641,9 +6641,9 @@ lazy-val@^1.0.3, 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#b3bf3f6d53410ff1c5415b51ca425341e364959f: lbry-redux@lbryio/lbry-redux#2930ad82a90ca91f6caf3761597ef9a67da7db66:
version "0.0.1" 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: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"