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, "indent": 0,
"jsx-quotes": ["error", "prefer-double"], "jsx-quotes": ["error", "prefer-double"],
"new-cap": 0, "new-cap": 0,
"no-console": 1,
"no-multi-spaces": 0, "no-multi-spaces": 0,
"no-redeclare": 0, "no-redeclare": 0,
"no-return-await": 0, "no-return-await": 0,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,13 +1,15 @@
// @flow // @flow
import type { Node } from 'react'; import type { Node } from 'react';
import React, { useEffect, useState } from 'react'; import React, { useEffect } from 'react';
import moment from 'moment'; import { withRouter } from 'react-router';
import usePersistedState from 'util/use-persisted-state'; import { buildClaimSearchCacheQuery, MATURE_TAGS } from 'lbry-redux';
import { MATURE_TAGS } from 'lbry-redux';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import moment from 'moment';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import Tag from 'component/tag'; import Tag from 'component/tag';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import { updateQueryParam } from 'util/query-params';
import { toCapitalCase } from 'util/string';
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
const TIME_DAY = 'day'; const TIME_DAY = 'day';
@ -29,78 +31,122 @@ const SEARCH_TIMES = [TIME_DAY, TIME_WEEK, TIME_MONTH, TIME_YEAR, TIME_ALL];
type Props = { type Props = {
uris: Array<string>, uris: Array<string>,
subscribedChannels: Array<Subscription>, subscribedChannels: Array<Subscription>,
doClaimSearch: (number, {}) => void, doClaimSearch: ({}) => void,
injectedItem: any, injectedItem: any,
tags: Array<string>, tags: Array<string>,
loading: boolean, loading: boolean,
personal: boolean, personalView: boolean,
doToggleTagFollow: string => void, doToggleTagFollow: string => void,
meta?: Node, meta?: Node,
showNsfw: boolean, showNsfw: boolean,
history: { action: string, push: string => void, replace: string => void },
location: { search: string, pathname: string },
claimSearchByQuery: {
[string]: Array<string>,
},
}; };
function ClaimListDiscover(props: Props) { function ClaimListDiscover(props: Props) {
const { doClaimSearch, uris, tags, loading, personal, injectedItem, meta, subscribedChannels, showNsfw } = props; const {
const [personalSort, setPersonalSort] = usePersistedState('claim-list-discover:personalSort', SEARCH_SORT_YOU); doClaimSearch,
const [typeSort, setTypeSort] = usePersistedState('claim-list-discover:typeSort', TYPE_TRENDING); claimSearchByQuery,
const [timeSort, setTimeSort] = usePersistedState('claim-list-discover:timeSort', TIME_WEEK); tags,
const [page, setPage] = useState(1); loading,
personalView,
const toCapitalCase = string => string.charAt(0).toUpperCase() + string.slice(1); injectedItem,
const tagsString = tags.join(','); meta,
const channelsIdString = subscribedChannels.map(channel => channel.uri.split('#')[1]).join(','); subscribedChannels,
useEffect(() => { 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: { const options: {
page_size: number, page_size: number,
any_tags?: Array<string>, page: number,
order_by?: Array<string>, no_totals: boolean,
channel_ids?: Array<string>, any_tags: Array<string>,
channel_ids: Array<string>,
not_tags: Array<string>,
order_by: Array<string>,
release_time?: string, release_time?: string,
not_tags?: Array<string>, } = {
} = { page_size: PAGE_SIZE, page, no_totals: true }; page_size: PAGE_SIZE,
const newTags = tagsString.split(','); page,
const newChannelIds = channelsIdString.split(','); // 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 ((newTags && !personal) || (newTags && personal && personalSort === SEARCH_SORT_YOU)) { if (typeSort === TYPE_TOP && timeSort !== TIME_ALL) {
options.any_tags = newTags; options.release_time = `>${Math.floor(
} else if (personalSort === SEARCH_SORT_CHANNELS) {
options.channel_ids = newChannelIds;
}
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() moment()
.subtract(1, timeSort) .subtract(1, timeSort)
.unix() .unix()
); )}`;
options.release_time = `>${time}`; }
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);
} }
} }
doClaimSearch(20, options); useEffect(() => {
}, [personal, personalSort, typeSort, timeSort, doClaimSearch, page, tagsString, channelsIdString, showNsfw]); if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringForEffect);
function getLabel(type) { doClaimSearch(searchOptions);
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 = ( const header = (
<h1 className="card__title--flex"> <h1 className="card__title--flex">
@ -109,10 +155,7 @@ function ClaimListDiscover(props: Props) {
type="select" type="select"
name="trending_sort" name="trending_sort"
value={typeSort} value={typeSort}
onChange={e => { onChange={e => handleTypeSort(e.target.value)}
resetList();
setTypeSort(e.target.value);
}}
> >
{SEARCH_TYPES.map(type => ( {SEARCH_TYPES.map(type => (
<option key={type} value={type}> <option key={type} value={type}>
@ -121,7 +164,7 @@ function ClaimListDiscover(props: Props) {
))} ))}
</FormField> </FormField>
<span>{__('For')}</span> <span>{__('For')}</span>
{!personal && tags && tags.length ? ( {!personalView && tags && tags.length ? (
tags.map(tag => <Tag key={tag} name={tag} disabled />) tags.map(tag => <Tag key={tag} name={tag} disabled />)
) : ( ) : (
<FormField <FormField
@ -130,13 +173,16 @@ function ClaimListDiscover(props: Props) {
className="claim-list__dropdown" className="claim-list__dropdown"
value={personalSort} value={personalSort}
onChange={e => { onChange={e => {
resetList(); handlePersonalSort(e.target.value);
setPersonalSort(e.target.value);
}} }}
> >
{SEARCH_FILTER_TYPES.map(type => ( {SEARCH_FILTER_TYPES.map(type => (
<option key={type} value={type}> <option key={type} value={type}>
{getLabel(type)} {type === SEARCH_SORT_ALL
? __('Everyone')
: type === SEARCH_SORT_YOU
? __('Tags You Follow')
: __('Channels You Follow')}
</option> </option>
))} ))}
</FormField> </FormField>
@ -147,10 +193,7 @@ function ClaimListDiscover(props: Props) {
type="select" type="select"
name="trending_time" name="trending_time"
value={timeSort} value={timeSort}
onChange={e => { onChange={e => handleTimeSort(e.target.value)}
resetList();
setTimeSort(e.target.value);
}}
> >
{SEARCH_TIMES.map(time => ( {SEARCH_TIMES.map(time => (
<option key={time} value={time}> <option key={time} value={time}>
@ -173,14 +216,14 @@ function ClaimListDiscover(props: Props) {
injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem} injectedItem={personalSort === SEARCH_SORT_YOU && injectedItem}
header={header} header={header}
headerAltControls={meta} headerAltControls={meta}
onScrollBottom={() => setPage(page + 1)} onScrollBottom={handleScrollBottom}
page={page} page={page}
pageSize={PAGE_SIZE} 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> </div>
); );
} }
export default ClaimListDiscover; export default withRouter(ClaimListDiscover);

View file

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

View file

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

View file

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

View file

@ -253,6 +253,12 @@ export const icons = {
<path d="M7 11V7a5 5 0 0 1 9.9-1" /> <path d="M7 11V7a5 5 0 0 1 9.9-1" />
</g> </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( [ICONS.SUPPORT]: buildIcon(
<g> <g>
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" /> <polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />

View file

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

View file

@ -1,2 +1,9 @@
import { connect } from 'react-redux';
import { selectScrollStartingPosition } from 'redux/selectors/app';
import Router from './view'; 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 * as PAGES from 'constants/pages';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { Route, Redirect, Switch, withRouter } from 'react-router-dom'; import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
@ -21,22 +22,28 @@ import NavigationHistory from 'page/navigationHistory';
import TagsPage from 'page/tags'; import TagsPage from 'page/tags';
import FollowingPage from 'page/following'; import FollowingPage from 'page/following';
const Scroll = withRouter(function ScrollWrapper(props) { // Tell the browser we are handling scroll restoration
const { pathname } = props.location; 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(() => { useEffect(() => {
// Auto scroll to the top of a window for new pages window.scrollTo(0, currentScroll);
// The browser will handle scrolling if it needs to, but }, [currentScroll, url]);
// for new pages, react-router maintains the current y scroll position
window.scrollTo(0, 0);
}, [pathname]);
return props.children;
});
export default function AppRouter() {
return ( return (
<Scroll>
<Switch> <Switch>
<Route path="/" exact component={DiscoverPage} /> <Route path="/" exact component={DiscoverPage} />
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} /> <Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
@ -64,6 +71,7 @@ export default function AppRouter() {
{/* Route not found. Mostly for people typing crazy urls into the url */} {/* Route not found. Mostly for people typing crazy urls into the url */}
<Route render={() => <Redirect to="/" />} /> <Route render={() => <Redirect to="/" />} />
</Switch> </Switch>
</Scroll>
); );
} }
export default withRouter(AppRouter);

View file

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

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

View file

@ -5,7 +5,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'; import UserEmailResetButton from 'component/userEmailResetButton';
type Props = { type Props = {
cancelButton: Node, cancelButton: Node,
@ -21,14 +21,6 @@ type Props = {
function UserEmail(props: Props) { function UserEmail(props: Props) {
const { email, user, accessToken, fetchAccessToken } = 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; let isVerified = false;
if (user) { if (user) {
@ -71,7 +63,7 @@ function UserEmail(props: Props) {
</React.Fragment> </React.Fragment>
} }
value={email} value={email}
inputButton={<Button button="inverse" label={__('Change')} {...buttonsProps} />} inputButton={<UserEmailResetButton button="inverse" />}
/> />
)} )}
<p className="help"> <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 // @flow
import * as React from 'react'; import * as React from 'react';
import Button from 'component/button'; import Button from 'component/button';
import UserEmailResetButton from 'component/userEmailResetButton';
type Props = { type Props = {
cancelButton: React.Node,
email: string, email: string,
resendVerificationEmail: string => void, resendVerificationEmail: string => void,
checkEmailVerified: () => void, checkEmailVerified: () => void,
@ -47,7 +47,7 @@ class UserEmailVerify extends React.PureComponent<Props> {
emailVerifyCheckInterval: ?IntervalID; emailVerifyCheckInterval: ?IntervalID;
render() { render() {
const { cancelButton, email } = this.props; const { email } = this.props;
return ( return (
<React.Fragment> <React.Fragment>
@ -67,7 +67,8 @@ class UserEmailVerify extends React.PureComponent<Props> {
label={__('Resend verification email')} label={__('Resend verification email')}
onClick={this.handleResendVerificationEmail} onClick={this.handleResendVerificationEmail}
/> />
{cancelButton}
<UserEmailResetButton />
</div> </div>
<p className="help"> <p className="help">

View file

@ -3,10 +3,11 @@ import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React from 'react'; import React from 'react';
import classnames from 'classnames'; 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 Icon from 'component/common/icon';
import { parseQueryParams } from 'util/query-params';
import Autocomplete from './internal/autocomplete'; import Autocomplete from './internal/autocomplete';
import Tag from 'component/tag';
const L_KEY_CODE = 76; const L_KEY_CODE = 76;
const ESC_KEY_CODE = 27; const ESC_KEY_CODE = 27;
@ -22,6 +23,7 @@ type Props = {
doBlur: () => void, doBlur: () => void,
focused: boolean, focused: boolean,
doShowSnackBar: string => void, doShowSnackBar: string => void,
history: { push: string => void },
}; };
type State = { type State = {
@ -51,10 +53,12 @@ class WunderBar extends React.PureComponent<Props, State> {
getSuggestionIcon = (type: string) => { getSuggestionIcon = (type: string) => {
switch (type) { switch (type) {
case 'file': case SEARCH_TYPES.FILE:
return ICONS.FILE; return ICONS.FILE;
case 'channel': case SEARCH_TYPES.CHANNEL:
return ICONS.CHANNEL; return ICONS.CHANNEL;
case SEARCH_TYPES.TAG:
return ICONS.TAG;
default: default:
return ICONS.SEARCH; return ICONS.SEARCH;
} }
@ -90,7 +94,7 @@ class WunderBar extends React.PureComponent<Props, State> {
} }
handleSubmit(value: string, suggestion?: { value: string, type: string }) { 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 query = value.trim();
const showSnackError = () => { const showSnackError = () => {
@ -99,8 +103,10 @@ class WunderBar extends React.PureComponent<Props, State> {
// User selected a suggestion // User selected a suggestion
if (suggestion) { if (suggestion) {
if (suggestion.type === 'search') { if (suggestion.type === SEARCH_TYPES.SEARCH) {
onSearch(query); onSearch(query);
} else if (suggestion.type === SEARCH_TYPES.TAG) {
history.push(`/$/${PAGES.TAGS}?t=${suggestion.value}`);
} else if (isURIValid(query)) { } else if (isURIValid(query)) {
const uri = normalizeURI(query); const uri = normalizeURI(query);
onSubmit(uri); onSubmit(uri);
@ -157,18 +163,22 @@ class WunderBar extends React.PureComponent<Props, State> {
)} )}
renderItem={({ value, type }, isHighlighted) => ( renderItem={({ value, type }, isHighlighted) => (
<div <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', { className={classnames('wunderbar__suggestion', {
'wunderbar__active-suggestion': isHighlighted, 'wunderbar__active-suggestion': isHighlighted,
})} })}
> >
<Icon icon={this.getSuggestionIcon(type)} /> <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 && ( {isHighlighted && (
<span className="wunderbar__suggestion-label--action"> <span className="wunderbar__suggestion-label--action">
{type === SEARCH_TYPES.SEARCH && __('Search')} {type === SEARCH_TYPES.SEARCH && __('Search')}
{type === SEARCH_TYPES.CHANNEL && __('View channel')} {type === SEARCH_TYPES.CHANNEL && __('View channel')}
{type === SEARCH_TYPES.FILE && __('View file')} {type === SEARCH_TYPES.FILE && __('View file')}
{type === SEARCH_TYPES.TAG && __('View Tag')}
</span> </span>
)} )}
</div> </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 LIGHT = 'Sun';
export const DARK = 'Moon'; export const DARK = 'Moon';
export const LIBRARY = 'Folder'; export const LIBRARY = 'Folder';
export const TAG = 'Tag';
export const SUPPORT = 'TrendingUp'; export const SUPPORT = 'TrendingUp';

View file

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

View file

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

View file

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

View file

@ -173,7 +173,6 @@ class SettingsPage extends React.PureComponent<Props, State> {
</header> </header>
<div className="card__content"> <div className="card__content">
<Form>
<FileSelector <FileSelector
type="openDirectory" type="openDirectory"
currentPath={daemonSettings.download_dir} currentPath={daemonSettings.download_dir}
@ -182,7 +181,6 @@ class SettingsPage extends React.PureComponent<Props, State> {
}} }}
/> />
<p className="help">{__('LBRY downloads will be saved here.')}</p> <p className="help">{__('LBRY downloads will be saved here.')}</p>
</Form>
</div> </div>
</section> </section>

View file

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

View file

@ -63,6 +63,31 @@ const defaultState: AppState = {
isUpgradeSkipped: undefined, isUpgradeSkipped: undefined,
enhancedLayout: false, enhancedLayout: false,
searchOptionsExpanded: 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 => reducers[ACTIONS.DAEMON_READY] = state =>

View file

@ -123,3 +123,8 @@ export const selectSearchOptionsExpanded = createSelector(
selectState, selectState,
state => state.searchOptionsExpanded 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) history => history.find(i => i.uri === uri)
); );
export const makeSelectHasVisitedUri = (uri: string) =>
createSelector(
makeSelectHistoryForUri(uri),
history => Boolean(history)
);
export const selectRecentHistory = createSelector( export const selectRecentHistory = createSelector(
selectHistory, selectHistory,
history => { history => {

View file

@ -14,7 +14,7 @@
[data-mode='dark'] & { [data-mode='dark'] & {
color: var(--dm-color-01); 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 { svg {
stroke: $lbry-black; 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); line-height: var(--button-height);
border-radius: var(--button-radius); border-radius: var(--button-radius);
font-size: 1.1rem; font-size: 1.1rem;
padding-top: 0; padding: 0 var(--spacing-medium);
padding-bottom: 0;
box-sizing: border-box; box-sizing: border-box;
} }

View file

@ -24,6 +24,13 @@
&:hover { &:hover {
color: $lbry-teal-1; 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 // 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; cursor: pointer;
opacity: 0.6; opacity: 0.6;
@ -166,8 +184,5 @@
.claim-preview-title { .claim-preview-title {
font-weight: 600; font-weight: 600;
margin-right: auto; margin-right: auto;
} padding-right: var(--spacing-medium);
.claim-preview-tags {
margin-left: 0;
} }

View file

@ -1,5 +1,16 @@
@import '~@lbry/components/sass/form/_index.scss'; @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 { textarea {
&::placeholder { &::placeholder {
opacity: 0.4; 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-top-left-radius: 0;
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
border-top-right-radius: var(--input-border-radius); border-top-right-radius: var(--input-border-radius);
border-bottom-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 { .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'] & { [data-mode='dark'] & {
background-color: var(--dm-color-08); background-color: var(--dm-color-08);
} }
} }
.main-wrapper-inner { .main-wrapper__inner {
display: flex; display: flex;
align-items: flex-start;
min-height: 100vh;
max-width: 1200px; max-width: 1200px;
width: 100%;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
margin-top: var(--spacing-large); padding-left: var(--spacing-large);
position: relative; padding-right: var(--spacing-large);
padding-bottom: var(--spacing-main-padding);
} }
.main { .main {
min-width: 0; min-width: 0;
width: calc(100% - var(--side-nav-width) - var(--spacing-main-padding)); width: calc(100% - var(--side-nav-width) - var(--spacing-main-padding));
position: relative; position: relative;
margin-top: calc(var(--header-height) + var(--spacing-large));
margin-right: var(--spacing-main-padding);
@media (max-width: 600px) { @media (max-width: 600px) {
width: 100%; width: 100%;
@ -67,4 +62,8 @@
svg { svg {
stroke: $lbry-white; 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 { .navigation {
width: var(--side-nav-width); width: var(--side-nav-width);
padding-bottom: var(--spacing-main-padding);
font-size: 1.4rem; font-size: 1.4rem;
@media (max-width: 600px) { @media (max-width: 600px) {

View file

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

View file

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

View file

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

View file

@ -26,3 +26,14 @@ export function toQueryString(params) {
return parts.join('&'); 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; const refElement = ref.current;
if (refElement) { if (refElement) {
refElement.addEventListener('mouseover', handleHover); refElement.addEventListener('mouseenter', handleHover);
refElement.addEventListener('mouseleave', handleHover); refElement.addEventListener('mouseleave', handleHover);
return () => { return () => {
refElement.removeEventListener('mouseover', handleHover); refElement.removeEventListener('mouseenter', handleHover);
refElement.removeEventListener('mouseleave', handleHover); refElement.removeEventListener('mouseleave', handleHover);
}; };
} }

View file

@ -8,6 +8,7 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<!-- <!--
Primary definition for this is in webpack.web.config.js Primary definition for this is in webpack.web.config.js
We can't access it here because webpack isn't running on this file 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", "Confirm Claim Revoke": "Confirm Claim Revoke",
"Are you sure you want to remove this support?": "Are you sure you want to remove this support?", "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.", "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: dependencies:
regenerator-runtime "^0.13.2" 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": "@babel/runtime@^7.4.3":
version "7.4.4" version "7.4.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.4.tgz#dc2e34982eb236803aa27a07fea6857af1b9171d" 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" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#bb82aed61a5569e565daa784eb25fc1d639c0c22: lbry-redux@lbryio/lbry-redux#5080eb3ea1f09ce03c4f50d9224feddf737628d3:
version "0.0.1" 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: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"
@ -9578,6 +9585,15 @@ react-spring@^8.0.20:
"@babel/runtime" "^7.3.1" "@babel/runtime" "^7.3.1"
prop-types "^15.5.8" 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: react-toggle@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/react-toggle/-/react-toggle-4.0.2.tgz#77f487860efb87fafd197672a2db8c885be1440f" 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" resolved "https://registry.yarnpkg.com/reselect/-/reselect-3.0.1.tgz#efdaa98ea7451324d092b2b2163a6a1d7a9a2147"
integrity sha1-79qpjqdFEyTQkrKyFjpqHXqaIUc= 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: resolve-cwd@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"