Add tags to publish page, infinite scroll, navigation improvements #2593

Merged
neb-b merged 21 commits from infinite into master 2019-07-02 04:54:36 +02:00
33 changed files with 236 additions and 110 deletions
Showing only changes of commit 2ca254a573 - Show all commits

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);
} }
neb-b commented 2019-07-01 18:23:16 +02:00 (Migrated from github.com)
Review

@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 the claim_search params

@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 the `claim_search` params
kauffj commented 2019-07-01 22:54:49 +02:00 (Migrated from github.com)
Review

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 ( 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__subtext media__subtext--large">
<div className="media__actions media__actions--between"> <div className="media__subtitle__channel">
<div className="media__subtext media__subtext--large"> <UriIndicator uri={uri} link />
<div className="media__subtitle__channel">
<UriIndicator uri={uri} link />
</div>
{__('Published on')} <DateTime uri={uri} show={DateTime.SHOW_DATE} />
</div> </div>
{claimIsMine && (
<p>
{viewCount} {viewCount !== 1 ? __('Views') : __('View')}
</p>
)}
</div> </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>
<WalletBalance /> {IS_WEB && <UnsupportedOnWeb />}
<TransactionListRecent /> <div className={IS_WEB && 'card--disabled'}>
<WalletSend /> <WalletBalance />
<WalletAddress /> <TransactionListRecent />
<WalletSend />
<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"