so much discovery I can't take it #2617

Merged
neb-b merged 20 commits from fixes into master 2019-07-23 01:45:30 +02:00
47 changed files with 490 additions and 275 deletions

View file

@ -29,6 +29,7 @@
"indent": 0,
"jsx-quotes": ["error", "prefer-double"],
"new-cap": 0,
"no-console": 1,
"no-multi-spaces": 0,
"no-redeclare": 0,
"no-return-await": 0,

View file

@ -1,6 +1,6 @@
{
"name": "LBRY",
"version": "0.34.0-rc.5",
"version": "0.34.0-rc.9",
"description": "A browser for the LBRY network, a digital marketplace controlled by its users.",
"keywords": [
"lbry"
@ -124,7 +124,7 @@
"jsmediatags": "^3.8.1",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#b07dfa172a526aa5e09af57bb6cf33790c8d0c91",
"lbry-redux": "lbryio/lbry-redux#5080eb3ea1f09ce03c4f50d9224feddf737628d3",
"lbryinc": "lbryio/lbryinc#a93596c51c8fb0a226cb84df04c26a6bb60a45fb",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",
@ -158,6 +158,7 @@
"react-router-dom": "^5.0.0",
"react-simplemde-editor": "^4.0.0",
"react-spring": "^8.0.20",
"react-sticky-box": "^0.8.0",
"react-toggle": "^4.0.2",
"redux": "^3.6.0",
"redux-persist": "^4.8.0",
@ -199,8 +200,8 @@
"yarn": "^1.3"
},
"lbrySettings": {
"lbrynetDaemonVersion": "0.38.0",
"lbrynetDaemonUrlTemplate": "https://github.com/lbryio/lbry/releases/download/vDAEMONVER/lbrynet-OSNAME.zip",
"lbrynetDaemonVersion": "0.38.1",
"lbrynetDaemonUrlTemplate": "http://build.lbry.io/daemon/build-11443_commit-eae4ed7_branch-master/lbrynet-OSNAME.zip",
"lbrynetDaemonDir": "static/daemon",
"lbrynetDaemonFileName": "lbrynet"
}

View file

@ -51,14 +51,12 @@ function App(props: Props) {
}, [userId]);
return (
<div ref={appRef} onContextMenu={e => openContextMenu(e)}>
<div className={MAIN_WRAPPER_CLASS} ref={appRef} onContextMenu={e => openContextMenu(e)}>
<Header />
<div className={MAIN_WRAPPER_CLASS}>
<div className="main-wrapper-inner">
<Router />
<SideBar />
</div>
<div className="main-wrapper__inner">
<Router />
<SideBar />
</div>
<ModalRouter />

View file

@ -2,25 +2,33 @@ import { connect } from 'react-redux';
import { doFetchClaimsByChannel } from 'redux/actions/content';
import { PAGE_SIZE } from 'constants/claim';
import {
makeSelectClaimsInChannelForCurrentPageState,
makeSelectClaimsInChannelForPage,
makeSelectFetchingChannelClaims,
makeSelectClaimIsMine,
makeSelectTotalPagesForChannel,
} from 'lbry-redux';
import { withRouter } from 'react-router';
import ChannelPage from './view';
const select = (state, props) => ({
claimsInChannel: makeSelectClaimsInChannelForCurrentPageState(props.uri)(state),
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
totalPages: makeSelectTotalPagesForChannel(props.uri, PAGE_SIZE)(state),
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
});
const select = (state, props) => {
const { search } = props.location;
const urlParams = new URLSearchParams(search);
const page = urlParams.get('page') || 0;
return {
claimsInChannel: makeSelectClaimsInChannelForPage(props.uri, page)(state),
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
totalPages: makeSelectTotalPagesForChannel(props.uri, PAGE_SIZE)(state),
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
};
};
const perform = dispatch => ({
fetchClaims: (uri, page) => dispatch(doFetchClaimsByChannel(uri, page)),
});
export default connect(
select,
perform
)(ChannelPage);
export default withRouter(
connect(
select,
perform
)(ChannelPage)
);

View file

@ -118,14 +118,14 @@ function ChannelForm(props: Props) {
onUpdate={v => handleThumbnailChange(v)}
currentValue={params.thumbnail}
assetName={'Thumbnail'}
recommended={'(400x400)'}
recommended={'(300 x 300)'}
/>
<SelectAsset
onUpdate={v => handleCoverChange(v)}
currentValue={params.cover}
assetName={'Cover'}
recommended={'(1000x300)'}
recommended={'(1000 x 160)'}
/>
<FormField

View file

@ -39,18 +39,16 @@ export default function ClaimList(props: Props) {
type,
header,
onScrollBottom,
page,
pageSize,
} = props;
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
const hasUris = uris && !!uris.length;
const sortedUris = (hasUris && (currentSort === SORT_NEW ? uris : uris.slice().reverse())) || [];
const urisLength = (uris && uris.length) || 0;
const sortedUris = (urisLength > 0 && (currentSort === SORT_NEW ? uris : uris.slice().reverse())) || [];
function handleSortChange() {
setCurrentSort(currentSort === SORT_NEW ? SORT_OLD : SORT_NEW);
}
const urisLength = uris && uris.length;
useEffect(() => {
function handleScroll(e) {
if (pageSize && onScrollBottom) {
@ -71,7 +69,7 @@ export default function ClaimList(props: Props) {
window.removeEventListener('scroll', handleScroll);
};
}
}, [loading, onScrollBottom, urisLength]);
}, [loading, onScrollBottom, urisLength, pageSize]);
return (
<section
@ -100,17 +98,17 @@ export default function ClaimList(props: Props) {
</div>
</div>
)}
{hasUris && (
{urisLength > 0 && (
<ul>
{sortedUris.map((uri, index) => (
<React.Fragment key={uri}>
<ClaimPreview uri={uri} type={type} placeholder={loading && (!page || page === 1)} />
<ClaimPreview uri={uri} type={type} />
{index === 4 && injectedItem && <li className="claim-preview--injected">{injectedItem}</li>}
</React.Fragment>
))}
</ul>
)}
{!hasUris && !loading && <h2 className="main--empty empty">{empty || __('No results')}</h2>}
{urisLength === 0 && !loading && <h2 className="main--empty empty">{empty || __('No results')}</h2>}
</section>
);
}

View file

@ -1,12 +1,12 @@
import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { doClaimSearch, selectLastClaimSearchUris, selectFetchingClaimSearch, doToggleTagFollow } from 'lbry-redux';
import { doClaimSearch, selectClaimSearchByQuery, selectFetchingClaimSearch, doToggleTagFollow } from 'lbry-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import ClaimListDiscover from './view';
const select = state => ({
uris: selectLastClaimSearchUris(state),
claimSearchByQuery: selectClaimSearchByQuery(state),
loading: selectFetchingClaimSearch(state),
subscribedChannels: selectSubscriptions(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_NSFW)(state),

View file

@ -1,13 +1,15 @@
// @flow
import type { Node } from 'react';
import React, { useEffect, useState } from 'react';
import moment from 'moment';
import usePersistedState from 'util/use-persisted-state';
import { MATURE_TAGS } from 'lbry-redux';
import React, { useEffect } from 'react';
import { withRouter } from 'react-router';
import { buildClaimSearchCacheQuery, MATURE_TAGS } from 'lbry-redux';
import { FormField } from 'component/common/form';
import moment from 'moment';
import ClaimList from 'component/claimList';
import Tag from 'component/tag';
import ClaimPreview from 'component/claimPreview';
import { updateQueryParam } from 'util/query-params';
import { toCapitalCase } from 'util/string';
const PAGE_SIZE = 20;
const TIME_DAY = 'day';
@ -29,78 +31,122 @@ const SEARCH_TIMES = [TIME_DAY, TIME_WEEK, TIME_MONTH, TIME_YEAR, TIME_ALL];
type Props = {
uris: Array<string>,
subscribedChannels: Array<Subscription>,
doClaimSearch: (number, {}) => void,
doClaimSearch: ({}) => void,
injectedItem: any,
tags: Array<string>,
loading: boolean,
personal: boolean,
personalView: boolean,
doToggleTagFollow: string => void,
meta?: Node,
showNsfw: boolean,
history: { action: string, push: string => void, replace: string => void },
location: { search: string, pathname: string },
claimSearchByQuery: {
[string]: Array<string>,
},
};
function ClaimListDiscover(props: Props) {
const { doClaimSearch, uris, tags, loading, personal, injectedItem, meta, subscribedChannels, showNsfw } = props;
const [personalSort, setPersonalSort] = usePersistedState('claim-list-discover:personalSort', SEARCH_SORT_YOU);
const [typeSort, setTypeSort] = usePersistedState('claim-list-discover:typeSort', TYPE_TRENDING);
const [timeSort, setTimeSort] = usePersistedState('claim-list-discover:timeSort', TIME_WEEK);
const [page, setPage] = useState(1);
const {
doClaimSearch,
claimSearchByQuery,
tags,
loading,
personalView,
injectedItem,
meta,
subscribedChannels,
showNsfw,
history,
location,
} = props;
const didNavigateForward = history.action === 'PUSH';
const { search, pathname } = location;
const urlParams = new URLSearchParams(search);
const personalSort = urlParams.get('sort') || SEARCH_SORT_YOU;
const typeSort = urlParams.get('type') || TYPE_TRENDING;
const timeSort = urlParams.get('time') || TIME_WEEK;
const page = Number(urlParams.get('page')) || 1;
const tagsInUrl = urlParams.get('t') || '';
const url = `${pathname}${search}`;
const options: {
page_size: number,
page: number,
no_totals: boolean,
any_tags: Array<string>,
channel_ids: Array<string>,
not_tags: Array<string>,
order_by: Array<string>,
release_time?: string,
} = {
page_size: PAGE_SIZE,
page,
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination
// it's faster, but we will need to remove it if we start using total_pages
no_totals: true,
any_tags: (personalView && personalSort === SEARCH_SORT_YOU) || !personalView ? tags : [],
channel_ids: personalSort === SEARCH_SORT_CHANNELS ? subscribedChannels.map(sub => sub.uri.split('#')[1]) : [],
not_tags: !showNsfw ? MATURE_TAGS : [],
order_by:
typeSort === TYPE_TRENDING
? ['trending_global', 'trending_mixed']
: typeSort === TYPE_NEW
? ['release_time']
: ['effective_amount'], // Sort by top
};
if (typeSort === TYPE_TOP && timeSort !== TIME_ALL) {
options.release_time = `>${Math.floor(
moment()
.subtract(1, timeSort)
.unix()
)}`;
}
const claimSearchCacheQuery = buildClaimSearchCacheQuery(options);
const uris = claimSearchByQuery[claimSearchCacheQuery] || [];
const shouldPerformSearch = uris.length === 0 || didNavigateForward || (!loading && uris.length < PAGE_SIZE * page);
// Don't use the query from buildClaimSearchCacheQuery for the effect since that doesn't include page & release_time
const optionsStringForEffect = JSON.stringify(options);
function getSearch() {
let search = `?`;
if (!personalView) {
search += `t=${tagsInUrl}&`;
}
return search;
}
function handleTypeSort(newTypeSort) {
let url = `${getSearch()}type=${newTypeSort}&sort=${personalSort}`;
if (newTypeSort === TYPE_TOP) {
url += `&time=${timeSort}`;
}
history.push(url);
}
function handlePersonalSort(newPersonalSort) {
history.push(`${getSearch()}type=${typeSort}&sort=${newPersonalSort}`);
}
function handleTimeSort(newTimeSort) {
history.push(`${getSearch()}type=${typeSort}&sort=${personalSort}&time=${newTimeSort}`);
}
function handleScrollBottom() {
if (!loading) {
const uri = updateQueryParam(url, 'page', page + 1);
history.replace(uri);
}
}
const toCapitalCase = string => string.charAt(0).toUpperCase() + string.slice(1);
const tagsString = tags.join(',');
const channelsIdString = subscribedChannels.map(channel => channel.uri.split('#')[1]).join(',');
useEffect(() => {
const options: {
page_size: number,
any_tags?: Array<string>,
order_by?: Array<string>,
channel_ids?: Array<string>,
release_time?: string,
not_tags?: Array<string>,
} = { page_size: PAGE_SIZE, page, no_totals: true };
const newTags = tagsString.split(',');
const newChannelIds = channelsIdString.split(',');
if ((newTags && !personal) || (newTags && personal && personalSort === SEARCH_SORT_YOU)) {
options.any_tags = newTags;
} else if (personalSort === SEARCH_SORT_CHANNELS) {
options.channel_ids = newChannelIds;
if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringForEffect);
doClaimSearch(searchOptions);
}
if (!showNsfw) {
options.not_tags = MATURE_TAGS;
}
if (typeSort === TYPE_TRENDING) {
options.order_by = ['trending_global', 'trending_mixed'];
} else if (typeSort === TYPE_NEW) {
options.order_by = ['release_time'];
} else if (typeSort === TYPE_TOP) {
options.order_by = ['effective_amount'];
if (timeSort !== TIME_ALL) {
const time = Math.floor(
moment()
.subtract(1, timeSort)
.unix()
);
options.release_time = `>${time}`;
}
}
doClaimSearch(20, options);
}, [personal, personalSort, typeSort, timeSort, doClaimSearch, page, tagsString, channelsIdString, showNsfw]);
function getLabel(type) {
if (type === SEARCH_SORT_ALL) {
return __('Everyone');
}
return type === SEARCH_SORT_YOU ? __('Tags You Follow') : __('Channels You Follow');
}
function resetList() {
setPage(1);
}
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]);
const header = (
<h1 className="card__title--flex">
@ -109,10 +155,7 @@ function ClaimListDiscover(props: Props) {
type="select"
name="trending_sort"
value={typeSort}
onChange={e => {
resetList();
setTypeSort(e.target.value);
}}
onChange={e => handleTypeSort(e.target.value)}
>
{SEARCH_TYPES.map(type => (
<option key={type} value={type}>
@ -121,7 +164,7 @@ function ClaimListDiscover(props: Props) {
))}
</FormField>
<span>{__('For')}</span>
{!personal && tags && tags.length ? (
{!personalView && tags && tags.length ? (
tags.map(tag => <Tag key={tag} name={tag} disabled />)
) : (
<FormField
@ -130,13 +173,16 @@ function ClaimListDiscover(props: Props) {
className="claim-list__dropdown"
value={personalSort}
onChange={e => {
resetList();
setPersonalSort(e.target.value);
handlePersonalSort(e.target.value);
}}
>
{SEARCH_FILTER_TYPES.map(type => (
<option key={type} value={type}>
{getLabel(type)}
{type === SEARCH_SORT_ALL
? __('Everyone')
: type === SEARCH_SORT_YOU
? __('Tags You Follow')
: __('Channels You Follow')}
</option>
))}
</FormField>
@ -147,10 +193,7 @@ function ClaimListDiscover(props: Props) {
type="select"
name="trending_time"
value={timeSort}
onChange={e => {
resetList();
setTimeSort(e.target.value);
}}
onChange={e => handleTimeSort(e.target.value)}
>
{SEARCH_TIMES.map(time => (
<option key={time} value={time}>
@ -173,14 +216,14 @@ function ClaimListDiscover(props: Props) {
injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem}
header={header}
headerAltControls={meta}
onScrollBottom={() => setPage(page + 1)}
onScrollBottom={handleScrollBottom}
page={page}
pageSize={PAGE_SIZE}
/>
{loading && page > 1 && new Array(PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder />)}
{loading && new Array(PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder />)}
</div>
);
}
export default ClaimListDiscover;
export default withRouter(ClaimListDiscover);

View file

@ -11,6 +11,7 @@ import {
} from 'lbry-redux';
import { selectBlackListedOutpoints, selectFilteredOutpoints } from 'lbryinc';
import { selectShowNsfw } from 'redux/selectors/settings';
import { makeSelectHasVisitedUri } from 'redux/selectors/content';
import ClaimPreview from './view';
const select = (state, props) => ({
@ -24,6 +25,7 @@ const select = (state, props) => ({
nsfw: makeSelectClaimIsNsfw(props.uri)(state),
blackListedOutpoints: selectBlackListedOutpoints(state),
filteredOutpoints: selectFilteredOutpoints(state),
hasVisitedUri: makeSelectHasVisitedUri(props.uri)(state),
});
const perform = dispatch => ({

View file

@ -29,6 +29,7 @@ type Props = {
nsfw: boolean,
placeholder: boolean,
type: string,
hasVisitedUri: boolean,
blackListedOutpoints: Array<{
txid: string,
nout: number,
@ -56,16 +57,23 @@ function ClaimPreview(props: Props) {
type,
blackListedOutpoints,
filteredOutpoints,
hasVisitedUri,
} = props;
const haventFetched = claim === undefined;
const abandoned = !isResolvingUri && !claim && !placeholder;
const { isChannel } = parseURI(uri);
const claimsInChannel = (claim && claim.meta.claims_in_channel) || 0;
let isValid;
try {
parseURI(uri);
isValid = true;
} catch (e) {
isValid = false;
}
const isChannel = isValid ? parseURI(uri).isChannel : false;
let shouldHide = abandoned || (!claimIsMine && obscureNsfw && nsfw);
// This will be replaced once blocking is done at the wallet server level
if (claim && !shouldHide && blackListedOutpoints) {
shouldHide = blackListedOutpoints.some(outpoint => outpoint.txid === claim.txid && outpoint.nout === claim.nout);
}
@ -89,16 +97,16 @@ function ClaimPreview(props: Props) {
}
useEffect(() => {
if (!isResolvingUri && haventFetched && uri) {
if (isValid && !isResolvingUri && haventFetched && uri) {
resolveUri(uri);
}
}, [isResolvingUri, uri, resolveUri, haventFetched]);
}, [isValid, isResolvingUri, uri, resolveUri, haventFetched]);
if (shouldHide) {
return null;
}
if (placeholder || isResolvingUri) {
if (placeholder || (isResolvingUri && !claim)) {
return (
<li className="claim-preview" disabled>
<div className="placeholder media__thumb" />
@ -117,7 +125,8 @@ function ClaimPreview(props: Props) {
onContextMenu={handleContextMenu}
className={classnames('claim-preview', {
'claim-preview--large': type === 'large',
'claim-list__pending': pending,
'claim-preview--visited': !isChannel && hasVisitedUri,
'claim-preview--pending': pending,
})}
>
{isChannel ? <ChannelThumbnail uri={uri} /> : <CardMedia thumbnail={thumbnail} />}

View file

@ -30,6 +30,7 @@ export function CommentCreate(props: Props) {
function handleCommentAck(event) {
setCommentAck(true);
}
function handleSubmit() {
if (channel !== CHANNEL_NEW && commentValue.length) createComment(commentValue, claimId, channel);
setCommentValue('');

View file

@ -253,6 +253,12 @@ export const icons = {
<path d="M7 11V7a5 5 0 0 1 9.9-1" />
</g>
),
[ICONS.TAG]: buildIcon(
<g>
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
<line x1="7" y1="7" x2="7" y2="7" />
</g>
),
[ICONS.SUPPORT]: buildIcon(
<g>
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />

View file

@ -15,7 +15,7 @@ type Props = {
isUpgradeAvailable: boolean,
roundedBalance: number,
downloadUpgradeRequested: any => void,
history: { push: string => void },
history: { push: string => void, goBack: () => void, goForward: () => void },
currentTheme: string,
automaticDarkModeEnabled: boolean,
setClientSetting: (string, boolean | string) => void,
@ -51,7 +51,7 @@ const Header = (props: Props) => {
<Button
className="header__navigation-item header__navigation-item--back"
description={__('Navigate back')}
onClick={() => window.history.back()}
onClick={() => history.goBack()}
icon={ICONS.ARROW_LEFT}
iconSize={18}
/>
@ -59,7 +59,7 @@ const Header = (props: Props) => {
<Button
className="header__navigation-item header__navigation-item--forward"
description={__('Navigate forward')}
onClick={() => window.history.forward()}
onClick={() => history.goForward()}
icon={ICONS.ARROW_RIGHT}
iconSize={18}
/>

View file

@ -1,2 +1,9 @@
import { connect } from 'react-redux';
import { selectScrollStartingPosition } from 'redux/selectors/app';
import Router from './view';
export default Router;
const select = state => ({
currentScroll: selectScrollStartingPosition(state),
});
export default connect(select)(Router);

View file

@ -1,3 +1,4 @@
// @flow
import * as PAGES from 'constants/pages';
import React, { useEffect } from 'react';
import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
@ -21,49 +22,56 @@ import NavigationHistory from 'page/navigationHistory';
import TagsPage from 'page/tags';
import FollowingPage from 'page/following';
const Scroll = withRouter(function ScrollWrapper(props) {
const { pathname } = props.location;
// Tell the browser we are handling scroll restoration
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
type Props = {
currentScroll: number,
location: { pathname: string, search: string },
};
function AppRouter(props: Props) {
const { currentScroll, location } = props;
const { pathname, search } = location;
// Don't update the scroll position if only the `page` param changes
const url = `${pathname}${search.replace(/&?\??page=\d+/, '')}`;
useEffect(() => {
// Auto scroll to the top of a window for new pages
// The browser will handle scrolling if it needs to, but
// for new pages, react-router maintains the current y scroll position
window.scrollTo(0, 0);
}, [pathname]);
window.scrollTo(0, currentScroll);
}, [currentScroll, url]);
return props.children;
});
export default function AppRouter() {
return (
<Scroll>
<Switch>
<Route path="/" exact component={DiscoverPage} />
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} />
<Route path={`/$/${PAGES.INVITE}`} exact component={InvitePage} />
<Route path={`/$/${PAGES.DOWNLOADED}`} exact component={FileListDownloaded} />
<Route path={`/$/${PAGES.PUBLISHED}`} exact component={FileListPublished} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.PUBLISH}`} exact component={PublishPage} />
<Route path={`/$/${PAGES.REPORT}`} exact component={ReportPage} />
<Route path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
<Route path={`/$/${PAGES.LIBRARY}`} exact component={LibraryPage} />
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} />
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
{/* Below need to go at the end to make sure we don't match any of our pages first */}
<Route path="/:claimName" exact component={ShowPage} />
<Route path="/:claimName/:contentName" exact component={ShowPage} />
<Switch>
<Route path="/" exact component={DiscoverPage} />
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} />
<Route path={`/$/${PAGES.INVITE}`} exact component={InvitePage} />
<Route path={`/$/${PAGES.DOWNLOADED}`} exact component={FileListDownloaded} />
<Route path={`/$/${PAGES.PUBLISHED}`} exact component={FileListPublished} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.PUBLISH}`} exact component={PublishPage} />
<Route path={`/$/${PAGES.REPORT}`} exact component={ReportPage} />
<Route path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
<Route path={`/$/${PAGES.LIBRARY}`} exact component={LibraryPage} />
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} />
<Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
{/* Below need to go at the end to make sure we don't match any of our pages first */}
<Route path="/:claimName" exact component={ShowPage} />
<Route path="/:claimName/:contentName" exact component={ShowPage} />
{/* Route not found. Mostly for people typing crazy urls into the url */}
<Route render={() => <Redirect to="/" />} />
</Switch>
</Scroll>
{/* Route not found. Mostly for people typing crazy urls into the url */}
<Route render={() => <Redirect to="/" />} />
</Switch>
);
}
export default withRouter(AppRouter);

View file

@ -1,9 +1,10 @@
// @flow
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import * as React from 'react';
import React from 'react';
import Button from 'component/button';
import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext';
type Props = {
subscriptions: Array<Subscription>,
@ -12,21 +13,18 @@ type Props = {
function SideBar(props: Props) {
const { subscriptions, followedTags } = props;
const buildLink = (path, label, icon, guide) => ({
navigate: path ? `$/${path}` : '/',
label,
icon,
guide,
});
const renderLink = linkProps => (
<li key={linkProps.label}>
<Button {...linkProps} className="navigation__link" activeClass="navigation__link--active" />
</li>
);
function buildLink(path, label, icon, guide) {
return {
navigate: path ? `$/${path}` : '/',
label,
icon,
guide,
};
}
return (
<div className="navigation-wrapper">
<StickyBox offsetBottom={40} offsetTop={100}>
<nav className="navigation">
<ul className="navigation__links">
{[
@ -39,7 +37,14 @@ function SideBar(props: Props) {
{
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH),
},
].map(renderLink)}
{
...buildLink(PAGES.FOLLOWING, __('Customize'), ICONS.EDIT),
},
].map(linkProps => (
<li key={linkProps.label}>
<Button {...linkProps} className="navigation__link" activeClass="navigation__link--active" />
</li>
))}
</ul>
<ul className="navigation__links tags--vertical">
{followedTags.map(({ name }, key) => (
@ -61,7 +66,7 @@ function SideBar(props: Props) {
))}
</ul>
</nav>
</div>
</StickyBox>
);
}

View file

@ -221,11 +221,11 @@ export default class SplashScreen extends React.PureComponent<Props, State> {
:container {
perspective: 30vmin;
}
@place-cell: center;
@size: 100%;
box-shadow: @m2(0 0 50px var(--color));
box-shadow: @m2(0 0 50px var(--color));
will-change: transform, opacity;
animation: scale-up 12s linear infinite;
animation-delay: calc(-12s / @size() * @i());
@ -235,11 +235,11 @@ export default class SplashScreen extends React.PureComponent<Props, State> {
transform: translateZ(0) rotate(0);
opacity: 0;
}
10% {
opacity: 1;
10% {
opacity: 1;
}
95% {
transform:
transform:
translateZ(35vmin) rotateZ(@var(--deg));
}
}

View file

@ -44,10 +44,12 @@ class TransactionListItem extends React.PureComponent<Props> {
const { amount, claim_id: claimId, claim_name: name, date, fee, txid, type } = transaction;
// Ensure the claim name exists and is valid
let uri;
let claimName = name;
if (claimName) {
try {
({ claimName } = parseURI(name));
}
uri = buildURI({ claimName: claimName, claimId });
} catch (e) {}
const dateFormat = {
month: 'short',
@ -72,9 +74,7 @@ class TransactionListItem extends React.PureComponent<Props> {
</td>
<td className="table__item--actionable">
{reward && <span>{reward.reward_title}</span>}
{claimName && claimId && (
<Button button="link" navigate={buildURI({ claimName: claimName, claimId })} label={claimName} />
)}
{claimName && claimId ? <Button button="link" navigate={uri} label={claimName} /> : claimName}
</td>
<td>

View file

@ -5,7 +5,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';
import UserEmailResetButton from 'component/userEmailResetButton';
type Props = {
cancelButton: Node,
@ -21,14 +21,6 @@ type Props = {
function UserEmail(props: Props) {
const { email, user, accessToken, fetchAccessToken } = props;
const buttonsProps = IS_WEB
? {
onClick: () => {
document.cookie = cookie.serialize('auth_token', '');
window.location.reload();
},
}
: { href: 'https://lbry.com/faq/how-to-change-email' };
let isVerified = false;
if (user) {
@ -71,7 +63,7 @@ function UserEmail(props: Props) {
</React.Fragment>
}
value={email}
inputButton={<Button button="inverse" label={__('Change')} {...buttonsProps} />}
inputButton={<UserEmailResetButton button="inverse" />}
/>
)}
<p className="help">

View file

@ -0,0 +1,11 @@
import { connect } from 'react-redux';
import UserEmailResetButton from './view';
const select = state => ({});
const perform = dispatch => ({});
export default connect(
select,
perform
)(UserEmailResetButton);

View file

@ -0,0 +1,24 @@
// @flow
import React from 'react';
import Button from 'component/button';
import cookie from 'cookie';
type Props = {
button: string,
};
function UserEmailResetButton(props: Props) {
const { button = 'link' } = props;
const buttonsProps = IS_WEB
? {
onClick: () => {
document.cookie = cookie.serialize('auth_token', '');
window.location.reload();
},
}
: { href: 'https://lbry.com/faq/how-to-change-email' };
return <Button button={button} label={__('Change')} {...buttonsProps} />;
}
export default UserEmailResetButton;

View file

@ -1,9 +1,9 @@
// @flow
import * as React from 'react';
import Button from 'component/button';
import UserEmailResetButton from 'component/userEmailResetButton';
type Props = {
cancelButton: React.Node,
email: string,
resendVerificationEmail: string => void,
checkEmailVerified: () => void,
@ -47,7 +47,7 @@ class UserEmailVerify extends React.PureComponent<Props> {
emailVerifyCheckInterval: ?IntervalID;
render() {
const { cancelButton, email } = this.props;
const { email } = this.props;
return (
<React.Fragment>
@ -67,7 +67,8 @@ class UserEmailVerify extends React.PureComponent<Props> {
label={__('Resend verification email')}
onClick={this.handleResendVerificationEmail}
/>
{cancelButton}
<UserEmailResetButton />
</div>
<p className="help">

View file

@ -3,10 +3,11 @@ import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import React from 'react';
import classnames from 'classnames';
import { normalizeURI, SEARCH_TYPES, isURIValid, buildURI } from 'lbry-redux';
import { normalizeURI, SEARCH_TYPES, isURIValid } from 'lbry-redux';
import { withRouter } from 'react-router';
import Icon from 'component/common/icon';
import { parseQueryParams } from 'util/query-params';
import Autocomplete from './internal/autocomplete';
import Tag from 'component/tag';
const L_KEY_CODE = 76;
const ESC_KEY_CODE = 27;
@ -22,6 +23,7 @@ type Props = {
doBlur: () => void,
focused: boolean,
doShowSnackBar: string => void,
history: { push: string => void },
};
type State = {
@ -51,10 +53,12 @@ class WunderBar extends React.PureComponent<Props, State> {
getSuggestionIcon = (type: string) => {
switch (type) {
case 'file':
case SEARCH_TYPES.FILE:
return ICONS.FILE;
case 'channel':
case SEARCH_TYPES.CHANNEL:
return ICONS.CHANNEL;
case SEARCH_TYPES.TAG:
return ICONS.TAG;
default:
return ICONS.SEARCH;
}
@ -90,7 +94,7 @@ class WunderBar extends React.PureComponent<Props, State> {
}
handleSubmit(value: string, suggestion?: { value: string, type: string }) {
const { onSubmit, onSearch, doShowSnackBar } = this.props;
const { onSubmit, onSearch, doShowSnackBar, history } = this.props;
const query = value.trim();
const showSnackError = () => {
@ -99,8 +103,10 @@ class WunderBar extends React.PureComponent<Props, State> {
// User selected a suggestion
if (suggestion) {
if (suggestion.type === 'search') {
if (suggestion.type === SEARCH_TYPES.SEARCH) {
onSearch(query);
} else if (suggestion.type === SEARCH_TYPES.TAG) {
history.push(`/$/${PAGES.TAGS}?t=${suggestion.value}`);
} else if (isURIValid(query)) {
const uri = normalizeURI(query);
onSubmit(uri);
@ -157,18 +163,22 @@ class WunderBar extends React.PureComponent<Props, State> {
)}
renderItem={({ value, type }, isHighlighted) => (
<div
key={value}
// Use value + type for key because there might be suggestions with same value but different type
key={`${value}-${type}`}
className={classnames('wunderbar__suggestion', {
'wunderbar__active-suggestion': isHighlighted,
})}
>
<Icon icon={this.getSuggestionIcon(type)} />
<span className="wunderbar__suggestion-label">{value}</span>
<span className="wunderbar__suggestion-label">
{type === SEARCH_TYPES.TAG ? <Tag name={value} /> : value}
</span>
{isHighlighted && (
<span className="wunderbar__suggestion-label--action">
{type === SEARCH_TYPES.SEARCH && __('Search')}
{type === SEARCH_TYPES.CHANNEL && __('View channel')}
{type === SEARCH_TYPES.FILE && __('View file')}
{type === SEARCH_TYPES.TAG && __('View Tag')}
</span>
)}
</div>
@ -179,4 +189,4 @@ class WunderBar extends React.PureComponent<Props, State> {
}
}
export default WunderBar;
export default withRouter(WunderBar);

View file

@ -69,4 +69,5 @@ export const MUSIC_EQUALIZER = 'Sliders';
export const LIGHT = 'Sun';
export const DARK = 'Moon';
export const LIBRARY = 'Folder';
export const TAG = 'Tag';
export const SUPPORT = 'TrendingUp';

View file

@ -5,6 +5,7 @@ import {
makeSelectThumbnailForUri,
makeSelectCoverForUri,
selectCurrentChannelPage,
makeSelectClaimForUri,
} from 'lbry-redux';
import ChannelPage from './view';
@ -14,6 +15,7 @@ const select = (state, props) => ({
cover: makeSelectCoverForUri(props.uri)(state),
channelIsMine: makeSelectClaimIsMine(props.uri)(state),
page: selectCurrentChannelPage(state),
claim: makeSelectClaimForUri(props.uri)(state),
});
export default connect(

View file

@ -20,6 +20,7 @@ const ABOUT_PAGE = `about`;
type Props = {
uri: string,
claim: ChannelClaim,
title: ?string,
cover: ?string,
thumbnail: ?string,
@ -31,12 +32,12 @@ type Props = {
};
function ChannelPage(props: Props) {
const { uri, title, cover, history, location, page, channelIsMine, thumbnail } = props;
const { uri, title, cover, history, location, page, channelIsMine, thumbnail, claim } = props;
const { channelName } = parseURI(uri);
const { search } = location;
const urlParams = new URLSearchParams(search);
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
const { permanent_url: permanentUrl } = claim;
const [editing, setEditing] = useState(false);
const [thumbPreview, setThumbPreview] = useState(thumbnail);
const [coverPreview, setCoverPreview] = useState(cover);
@ -90,7 +91,7 @@ function ChannelPage(props: Props) {
<Tab>{editing ? __('Editing Your Channel') : __('About')}</Tab>
<div className="card__actions">
<ShareButton uri={uri} />
<SubscribeButton uri={uri} />
<SubscribeButton uri={permanentUrl} />
</div>
</TabList>

View file

@ -16,7 +16,7 @@ function DiscoverPage(props: Props) {
return (
<Page>
<ClaimListDiscover
personal
personalView
tags={followedTags.map(tag => tag.name)}
meta={<Button button="link" label={__('Customize')} navigate={`/$/${PAGES.FOLLOWING}`} />}
injectedItem={<TagsSelect showClose title={__('Customize Your Homepage')} />}

View file

@ -173,16 +173,14 @@ class SettingsPage extends React.PureComponent<Props, State> {
</header>
<div className="card__content">
<Form>
<FileSelector
type="openDirectory"
currentPath={daemonSettings.download_dir}
onFileChosen={(newDirectory: string) => {
setDaemonSetting('download_dir', newDirectory);
}}
/>
<p className="help">{__('LBRY downloads will be saved here.')}</p>
</Form>
<FileSelector
type="openDirectory"
currentPath={daemonSettings.download_dir}
onFileChosen={(newDirectory: string) => {
setDaemonSetting('download_dir', newDirectory);
}}
/>
<p className="help">{__('LBRY downloads will be saved here.')}</p>
</div>
</section>

View file

@ -232,7 +232,6 @@ export const doPublish = () => (dispatch: Dispatch, getState: () => {}) => {
contentIsFree,
fee,
uri,
nsfw,
tags,
locations,
} = publishData;
@ -287,17 +286,6 @@ export const doPublish = () => (dispatch: Dispatch, getState: () => {}) => {
publishPayload.release_time = Number(myClaimForUri.value.release_time);
}
if (nsfw) {
if (!publishPayload.tags.includes('mature')) {
publishPayload.tags.push('mature');
}
} else {
const indexToRemove = publishPayload.tags.indexOf('mature');
if (indexToRemove > -1) {
publishPayload.tags.splice(indexToRemove, 1);
}
}
if (channelId) {
publishPayload.channel_id = channelId;
}
@ -347,7 +335,7 @@ export const doPublish = () => (dispatch: Dispatch, getState: () => {}) => {
dispatch({ type: ACTIONS.PUBLISH_FAIL });
dispatch(doError(error.message));
};
console.log('PP', publishPayload);
return Lbry.publish(publishPayload).then(success, failure);
};

View file

@ -63,6 +63,31 @@ const defaultState: AppState = {
isUpgradeSkipped: undefined,
enhancedLayout: false,
searchOptionsExpanded: false,
currentScroll: 0,
scrollHistory: [0],
};
// @@router comes from react-router
// This action is dispatched any time a user navigates forward or back
reducers['@@router/LOCATION_CHANGE'] = (state, action) => {
const { currentScroll } = state;
const scrollHistory = state.scrollHistory.slice();
const { action: name } = action.payload;
let newCurrentScroll = currentScroll;
if (name === 'PUSH') {
scrollHistory.push(window.scrollY);
newCurrentScroll = 0;
} else if (name === 'POP') {
newCurrentScroll = scrollHistory[scrollHistory.length - 1];
scrollHistory.pop();
}
return {
...state,
scrollHistory,
currentScroll: newCurrentScroll,
};
};
reducers[ACTIONS.DAEMON_READY] = state =>

View file

@ -123,3 +123,8 @@ export const selectSearchOptionsExpanded = createSelector(
selectState,
state => state.searchOptionsExpanded
);
export const selectScrollStartingPosition = createSelector(
selectState,
state => state.currentScroll
);

View file

@ -58,6 +58,12 @@ export const makeSelectHistoryForUri = (uri: string) =>
history => history.find(i => i.uri === uri)
);
export const makeSelectHasVisitedUri = (uri: string) =>
createSelector(
makeSelectHistoryForUri(uri),
history => Boolean(history)
);
export const selectRecentHistory = createSelector(
selectHistory,
history => {

View file

@ -14,7 +14,7 @@
[data-mode='dark'] & {
color: var(--dm-color-01);
background-color: rgba($lbry-teal-5, 0.3);
background-color: lighten(mix($lbry-black, $lbry-teal-5, 80%), 10%);
}
}
@ -31,4 +31,8 @@
svg {
stroke: $lbry-black;
}
[data-mode='dark'] & {
background-color: darken(mix($lbry-grape-1, $lbry-gray-5, 50%), 20%);
}
}

View file

@ -44,8 +44,7 @@
line-height: var(--button-height);
border-radius: var(--button-radius);
font-size: 1.1rem;
padding-top: 0;
padding-bottom: 0;
padding: 0 var(--spacing-medium);
box-sizing: border-box;
}

View file

@ -24,6 +24,13 @@
&:hover {
color: $lbry-teal-1;
}
[data-mode='dark'] & {
color: $lbry-teal-4;
&:hover {
color: $lbry-teal-1;
}
}
}
// Fix this in @lbry/components, we shouldn't need to be this specific
@ -129,7 +136,18 @@
}
}
.claim-list__pending {
.claim-preview--visited {
// Still keep the normal styles on hover regardless of if they have visited the claim
&:not(:hover) {
color: lighten($lbry-black, 35%);
[data-mode='dark'] & {
color: darken($lbry-white, 35%);
}
}
}
.claim-preview--pending {
cursor: pointer;
opacity: 0.6;
@ -166,8 +184,5 @@
.claim-preview-title {
font-weight: 600;
margin-right: auto;
}
.claim-preview-tags {
margin-left: 0;
padding-right: var(--spacing-medium);
}

View file

@ -1,5 +1,16 @@
@import '~@lbry/components/sass/form/_index.scss';
// Reset lbry components style that turns buttons inside of forms black
form {
.button--primary,
[type='submit'] {
&:not(:hover),
&:hover {
@extend .button--primary;
}
}
}
textarea {
&::placeholder {
opacity: 0.4;
@ -223,12 +234,16 @@ fieldset-section {
}
}
.button {
.button,
// specificity needed because of @lbry/component rules
// @lbry/componentsfixme
.button[type='submit']:not(:hover),
.button[type='submit']:hover {
border-color: $lbry-black;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-top-right-radius: var(--input-border-radius);
border-bottom-right-radius: var(--input-border-radius);
border-color: $lbry-black;
}
}

View file

@ -1,32 +1,27 @@
.main-wrapper {
position: absolute;
min-height: 100vh;
width: 100vw;
padding-top: var(--header-height);
padding-left: var(--spacing-large);
padding-right: var(--spacing-large);
padding-bottom: var(--spacing-large);
display: flex;
[data-mode='dark'] & {
background-color: var(--dm-color-08);
}
}
.main-wrapper-inner {
.main-wrapper__inner {
display: flex;
align-items: flex-start;
min-height: 100vh;
max-width: 1200px;
width: 100%;
margin-left: auto;
margin-right: auto;
margin-top: var(--spacing-large);
position: relative;
padding-left: var(--spacing-large);
padding-right: var(--spacing-large);
padding-bottom: var(--spacing-main-padding);
}
.main {
min-width: 0;
width: calc(100% - var(--side-nav-width) - var(--spacing-main-padding));
position: relative;
margin-top: calc(var(--header-height) + var(--spacing-large));
margin-right: var(--spacing-main-padding);
@media (max-width: 600px) {
width: 100%;
@ -67,4 +62,8 @@
svg {
stroke: $lbry-white;
}
[data-mode='dark'] & {
background-color: $lbry-teal-5;
}
}

View file

@ -1,13 +1,5 @@
.navigation-wrapper {
width: var(--side-nav-width);
left: calc(100% - var(--side-nav-width));
height: 100%;
position: absolute;
}
.navigation {
width: var(--side-nav-width);
padding-bottom: var(--spacing-main-padding);
font-size: 1.4rem;
@media (max-width: 600px) {

View file

@ -45,7 +45,6 @@ $main: $lbry-teal-5;
white-space: nowrap;
text-transform: lowercase;
font-size: 0.7em;
max-width: 10rem;
min-width: 0;
&:hover {

View file

@ -22,7 +22,7 @@
border-top: none;
[data-mode='dark'] & {
background-color: lighten($lbry-black, 10%);
background-color: var(--dm-color-05);
color: $lbry-white;
box-shadow: 0 10px 30px 2px $lbry-black;
border: 1px solid $lbry-gray-5;

View file

@ -6,6 +6,7 @@ html {
font-size: 12px;
height: 100%;
min-height: 100%;
overflow-x: hidden;
&[data-mode='dark'] {
@ -20,7 +21,6 @@ body {
font-weight: 400;
height: 100%;
line-height: 1.5;
overflow: hidden;
background-color: mix($lbry-white, $lbry-gray-1, 70%);
[data-mode='dark'] & {

View file

@ -26,3 +26,14 @@ export function toQueryString(params) {
return parts.join('&');
}
// https://stackoverflow.com/questions/5999118/how-can-i-add-or-update-a-query-string-parameter
export function updateQueryParam(uri, key, value) {
const re = new RegExp('([?&])' + key + '=.*?(&|$)', 'i');
const separator = uri.indexOf('?') !== -1 ? '&' : '?';
if (uri.match(re)) {
return uri.replace(re, '$1' + key + '=' + value + '$2');
} else {
return uri + separator + key + '=' + value;
}
}

5
src/ui/util/string.js Normal file
View file

@ -0,0 +1,5 @@
// @flow
export function toCapitalCase(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1);
}

View file

@ -10,10 +10,10 @@ export default function useHover(ref) {
const refElement = ref.current;
if (refElement) {
refElement.addEventListener('mouseover', handleHover);
refElement.addEventListener('mouseenter', handleHover);
refElement.addEventListener('mouseleave', handleHover);
return () => {
refElement.removeEventListener('mouseover', handleHover);
refElement.removeEventListener('mouseenter', handleHover);
refElement.removeEventListener('mouseleave', handleHover);
};
}

View file

@ -8,6 +8,7 @@
<body>
<div id="app"></div>
<!--
Primary definition for this is in webpack.web.config.js
We can't access it here because webpack isn't running on this file

View file

@ -575,5 +575,8 @@
"Confirm Claim Revoke": "Confirm Claim Revoke",
"Are you sure you want to remove this support?": "Are you sure you want to remove this support?",
"These credits are permanently yours and can be removed at any time. Removing this support will reduce the claim's discoverability and return the LBC to your spendable balance.": "These credits are permanently yours and can be removed at any time. Removing this support will reduce the claim's discoverability and return the LBC to your spendable balance.",
"The better your tags are, the easier it will be for people to discover your channel.": "The better your tags are, the easier it will be for people to discover your channel."
"Invalid character %s in name: %s.": "Invalid character %s in name: %s.",
"The better your tags are, the easier it will be for people to discover your channel.": "The better your tags are, the easier it will be for people to discover your channel.",
"Thumbnail (300 x 300)": "Thumbnail (300 x 300)",
"Cover (1000 x 160)": "Cover (1000 x 160)"
}

View file

@ -769,6 +769,13 @@
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.1.5":
version "7.5.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b"
integrity sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==
dependencies:
regenerator-runtime "^0.13.2"
"@babel/runtime@^7.4.3":
version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d"
@ -6646,9 +6653,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#bb82aed61a5569e565daa784eb25fc1d639c0c22:
lbry-redux@lbryio/lbry-redux#5080eb3ea1f09ce03c4f50d9224feddf737628d3:
version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/bb82aed61a5569e565daa784eb25fc1d639c0c22"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/5080eb3ea1f09ce03c4f50d9224feddf737628d3"
dependencies:
proxy-polyfill "0.1.6"
reselect "^3.0.0"
@ -9578,6 +9585,15 @@ react-spring@^8.0.20:
"@babel/runtime" "^7.3.1"
prop-types "^15.5.8"
react-sticky-box@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/react-sticky-box/-/react-sticky-box-0.8.0.tgz#1c191936af8f5420087b703ec6da4ef46060076c"
integrity sha512-al7fY+VzTKBgVrn14l21jQfhuG582Z6FD8tVbWVQDDqzcjLmUrFb+ljG2phxHhRRazg64L3yH4nOKjn78PZmag==
dependencies:
"@babel/runtime" "^7.1.5"
prop-types "^15.6.2"
resize-observer-polyfill "^1.5.1"
react-toggle@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.0.2.tgz#77f487860efb87fafd197672a2db8c885be1440f"
@ -10032,6 +10048,11 @@ reselect@^3.0.0:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc=
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"