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
29 changed files with 329 additions and 186 deletions
Showing only changes of commit 1015abbb87 - Show all commits

View file

@ -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",

View file

@ -51,14 +51,12 @@ function App(props: Props) {
}, [userId]); }, [userId]);
return ( return (
<div ref={appRef} onContextMenu={e => openContextMenu(e)}> <div className="main-wrapper" 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 />

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';
@ -33,75 +35,118 @@ type Props = {
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,
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(() => { useEffect(() => {
const options: { if (shouldPerformSearch) {
page_size: number, const searchOptions = JSON.parse(optionsStringForEffect);
any_tags?: Array<string>, doClaimSearch(searchOptions);
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;
} }
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect]);
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}`;
}
}
options.page_size = PAGE_SIZE;
doClaimSearch(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);
}
const header = ( const header = (
<h1 className="card__title--flex"> <h1 className="card__title--flex">
@ -110,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}>
@ -122,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
@ -131,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>
@ -148,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}>
@ -174,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

@ -100,13 +100,13 @@ function ClaimPreview(props: Props) {
if (isValid && !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" />

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

@ -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,10 @@
import { connect } from 'react-redux';
import Router from './view'; import Router from './view';
export default Router;
const select = state => ({
scroll: state.app.scrollHistory[state.app.scrollHistory.length - 1],
scrollHistory: state.app.scrollHistory,
currentScroll: state.app.currentScroll || 0,
});
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,49 +22,56 @@ 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) { // Let app handle scroll
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} /> <Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={AuthPage} /> <Route path={`/$/${PAGES.INVITE}`} exact component={InvitePage} />
<Route path={`/$/${PAGES.INVITE}`} exact component={InvitePage} /> <Route path={`/$/${PAGES.DOWNLOADED}`} exact component={FileListDownloaded} />
<Route path={`/$/${PAGES.DOWNLOADED}`} exact component={FileListDownloaded} /> <Route path={`/$/${PAGES.PUBLISHED}`} exact component={FileListPublished} />
<Route path={`/$/${PAGES.PUBLISHED}`} exact component={FileListPublished} /> <Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} /> <Route path={`/$/${PAGES.PUBLISH}`} exact component={PublishPage} />
<Route path={`/$/${PAGES.PUBLISH}`} exact component={PublishPage} /> <Route path={`/$/${PAGES.REPORT}`} exact component={ReportPage} />
<Route path={`/$/${PAGES.REPORT}`} exact component={ReportPage} /> <Route path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} />
<Route path={`/$/${PAGES.REWARDS}`} exact component={RewardsPage} /> <Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} /> <Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} />
<Route path={`/$/${PAGES.SETTINGS}`} exact component={SettingsPage} /> <Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} />
<Route path={`/$/${PAGES.TRANSACTIONS}`} exact component={TransactionHistoryPage} /> <Route path={`/$/${PAGES.LIBRARY}`} exact component={LibraryPage} />
<Route path={`/$/${PAGES.LIBRARY}`} exact component={LibraryPage} /> <Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} />
<Route path={`/$/${PAGES.ACCOUNT}`} exact component={AccountPage} /> <Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} />
<Route path={`/$/${PAGES.LIBRARY}/all`} exact component={NavigationHistory} /> <Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} /> <Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} />
<Route path={`/$/${PAGES.FOLLOWING}`} exact component={FollowingPage} /> <Route path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />
<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 */}
{/* 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" exact component={ShowPage} /> <Route path="/:claimName/:contentName" exact component={ShowPage} />
<Route path="/:claimName/:contentName" exact component={ShowPage} />
{/* 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) => ({
navigate: path ? `$/${path}` : '/',
label,
icon,
guide,
});
const renderLink = linkProps => ( function buildLink(path, label, icon, guide) {
<li key={linkProps.label}> return {
<Button {...linkProps} className="navigation__link" activeClass="navigation__link--active" /> navigate: path ? `$/${path}` : '/',
</li> label,
); icon,
guide,
};
}
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

@ -52,6 +52,8 @@ export default class SplashScreen extends React.PureComponent<Props, State> {
} }
componentDidMount() { componentDidMount() {
this.props.onReadyToLaunch();
const { checkDaemonVersion } = this.props; const { checkDaemonVersion } = this.props;
this.adjustErrorTimeout(); this.adjustErrorTimeout();
Lbry.connect() Lbry.connect()

View file

@ -63,7 +63,7 @@ class UriIndicator extends React.PureComponent<Props> {
return ( return (
<Button className="button--uri-indicator" navigate={channelLink}> <Button className="button--uri-indicator" navigate={channelLink}>
<Tooltip label={<ClaimPreview uri={channelLink} type="small" />}>{inner}</Tooltip> <Tooltip label={'test' || <ClaimPreview uri={channelLink} type="small" />}>{inner}</Tooltip>
</Button> </Button>
); );
} else { } else {

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

@ -63,6 +63,29 @@ const defaultState: AppState = {
isUpgradeSkipped: undefined, isUpgradeSkipped: undefined,
enhancedLayout: false, enhancedLayout: false,
searchOptionsExpanded: false, searchOptionsExpanded: false,
currentScroll: 0,
scrollHistory: [0],
};
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

@ -189,4 +189,9 @@
.claim-preview-tags { .claim-preview-tags {
margin-left: 0; margin-left: 0;
// change this
.tag {
max-width: 10rem;
}
} }

View file

@ -1,32 +1,25 @@
.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-bottom: var(--spacing-main-padding);
position: relative;
} }
.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%;

View file

@ -1,13 +1,12 @@
.navigation-wrapper { .navigation-wrapper {
width: var(--side-nav-width); // width: var(--side-nav-width);
left: calc(100% - var(--side-nav-width)); // left: calc(100% - var(--side-nav-width));
height: 100%; // height: 100%;
position: absolute; // 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

@ -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,13 @@ export function toQueryString(params) {
return parts.join('&'); return parts.join('&');
} }
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

@ -542,6 +542,7 @@
"Upgrade is ready to install": "Upgrade is ready to install", "Upgrade is ready to install": "Upgrade is ready to install",
"Upgrade is ready": "Upgrade is ready", "Upgrade is ready": "Upgrade is ready",
"Abandon the claim for this URI": "Abandon the claim for this URI", "Abandon the claim for this URI": "Abandon the claim for this URI",
<<<<<<< HEAD
"For video content, use MP4s in H264/AAC format for best compatibility.": "For video content, use MP4s in H264/AAC format for best compatibility.", "For video content, use MP4s in H264/AAC format for best compatibility.": "For video content, use MP4s in H264/AAC format for best compatibility.",
"Read the App Basics FAQ": "Read the App Basics FAQ", "Read the App Basics FAQ": "Read the App Basics FAQ",
"View all LBRY FAQs": "View all LBRY FAQs", "View all LBRY FAQs": "View all LBRY FAQs",
@ -577,3 +578,23 @@
"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." "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."
} }
=======
"View Tag": "View Tag",
"Invalid claim ID %s.": "Invalid claim ID %s.",
"Invalid character %s in name: %s.": "Invalid character %s in name: %s.",
"LBRY names cannot contain spaces or reserved symbols ($#@;/\"<>%{}|^~[]`)": "LBRY names cannot contain spaces or reserved symbols ($#@;/\"<>%{}|^~[]`)",
"No transactions.": "No transactions.",
"Today": "Today",
"This": "This",
"All time": "All time",
"Thumbnail (300 x 300)": "Thumbnail (300 x 300)",
"Cover (1000 x 160)": "Cover (1000 x 160)",
"No path provided after /": "No path provided after /",
"Connection Failure": "Connection Failure",
"Try closing all LBRY processes and starting again. If this still happens, your anti-virus software or firewall may be preventing LBRY from connecting. Contact hello@lbry.com if you think this is a software bug.": "Try closing all LBRY processes and starting again. If this still happens, your anti-virus software or firewall may be preventing LBRY from connecting. Contact hello@lbry.com if you think this is a software bug.",
"Unable to Authenticate": "Unable to Authenticate",
"Authentication Failure": "Authentication Failure",
"Reload": "Reload",
"If reloading does not fix this, or you see this at every start up, please email help@lbry.com.": "If reloading does not fix this, or you see this at every start up, please email help@lbry.com."
}
>>>>>>> restore that shit

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#eb3a7afc46ef69678149ad5caff3829473d75b43: lbry-redux@lbryio/lbry-redux#6406f184e8c87d79f4adb2b8792ff3643f4527ea:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/eb3a7afc46ef69678149ad5caff3829473d75b43" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/6406f184e8c87d79f4adb2b8792ff3643f4527ea"
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"