navigation redesign

This commit is contained in:
Sean Yesmunt 2020-01-02 11:30:27 -05:00
parent e7c1085faa
commit a8711c027f
33 changed files with 499 additions and 219 deletions

View file

@ -936,5 +936,12 @@
"Text copied": "Text copied",
"Rewards Disabled": "Rewards Disabled",
"Woah, you have a lot of friends! You've claimed the maximum amount of referral rewards. Check back soon to see if more are available!.": "Woah, you have a lot of friends! You've claimed the maximum amount of referral rewards. Check back soon to see if more are available!.",
"Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.": "Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%."
}
"Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.": "Wallet servers are used to relay data to and from the LBRY blockchain. They also determine what content shows in trending or is blocked. %learn_more%.",
"Your Tags": "Your Tags",
"All Content": "All Content",
"Claim Your 5 LBC Invite Reward": "Claim Your 5 LBC Invite Reward",
"Accepted": "Accepted",
"Claimable": "Claimable",
"Invite A Friend": "Invite A Friend",
"Navigation": "Navigation"
}

View file

@ -46,7 +46,7 @@ type LogPublishParams = {
let analyticsEnabled: boolean = true;
const analytics: Analytics = {
error: message => {
if (analyticsEnabled) {
if (analyticsEnabled && isProduction) {
Lbryio.call('event', 'desktop_error', { error_message: message });
}
},

View file

@ -147,6 +147,13 @@ function App(props: Props) {
}
}, [previousRewardApproved, isRewardApproved]);
// @if TARGET='app'
useEffect(() => {
console.log('update prefs');
updatePreferences();
}, []);
// @endif
// Keep this at the end to ensure initial setup effects are run first
useEffect(() => {
// Wait for balance to be populated on desktop so we know when we can begin syncing

View file

@ -7,14 +7,12 @@ import {
doToggleTagFollow,
selectBlockedChannels,
} from 'lbry-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import ClaimListDiscover from './view';
const select = state => ({
claimSearchByQuery: selectClaimSearchByQuery(state),
loading: selectFetchingClaimSearch(state),
subscribedChannels: selectSubscriptions(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
hiddenUris: selectBlockedChannels(state),
});

View file

@ -1,6 +1,6 @@
// @flow
import type { Node } from 'react';
import * as PAGES from 'constants/pages';
import classnames from 'classnames';
import React, { Fragment, useEffect, useState } from 'react';
import { withRouter } from 'react-router';
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
@ -8,7 +8,6 @@ import { FormField } from 'component/common/form';
import Button from 'component/button';
import moment from 'moment';
import ClaimList from 'component/claimList';
import Tag from 'component/tag';
import ClaimPreview from 'component/claimPreview';
import { toCapitalCase } from 'util/string';
import I18nMessage from 'component/i18nMessage';
@ -19,15 +18,12 @@ const TIME_WEEK = 'week';
const TIME_MONTH = 'month';
const TIME_YEAR = 'year';
const TIME_ALL = 'all';
const SEARCH_SORT_YOU = 'you';
const SEARCH_SORT_ALL = 'everyone';
const SEARCH_SORT_CHANNELS = 'channels';
const TYPE_TRENDING = 'trending';
const TYPE_TOP = 'top';
const TYPE_NEW = 'new';
const SEARCH_FILTER_TYPES = [SEARCH_SORT_YOU, SEARCH_SORT_CHANNELS, SEARCH_SORT_ALL];
const SEARCH_TYPES = [TYPE_TRENDING, TYPE_TOP, TYPE_NEW];
export const TYPE_TRENDING = 'trending';
export const TYPE_TOP = 'top';
export const TYPE_NEW = 'new';
const SEARCH_TYPES = [TYPE_TRENDING, TYPE_NEW, TYPE_TOP];
const SEARCH_TIMES = [TIME_DAY, TIME_WEEK, TIME_MONTH, TIME_YEAR, TIME_ALL];
type Props = {
@ -40,7 +36,6 @@ type Props = {
doToggleTagFollow: string => void,
meta?: Node,
showNsfw: boolean,
hideCustomization: boolean,
history: { action: string, push: string => void, replace: string => void },
location: { search: string, pathname: string },
claimSearchByQuery: {
@ -48,6 +43,8 @@ type Props = {
},
hiddenUris: Array<string>,
hiddenNsfwMessage?: Node,
channelIds?: Array<string>,
defaultTypeSort?: string,
};
function ClaimListDiscover(props: Props) {
@ -58,21 +55,20 @@ function ClaimListDiscover(props: Props) {
loading,
personalView,
meta,
subscribedChannels,
channelIds,
showNsfw,
history,
location,
hiddenUris,
hideCustomization,
hiddenNsfwMessage,
defaultTypeSort,
} = props;
const didNavigateForward = history.action === 'PUSH';
const [page, setPage] = useState(1);
const { search } = location;
const [forceRefresh, setForceRefresh] = useState();
const urlParams = new URLSearchParams(search);
const personalSort = urlParams.get('sort') || (hideCustomization ? SEARCH_SORT_ALL : SEARCH_SORT_YOU);
const typeSort = urlParams.get('type') || TYPE_TRENDING;
const typeSort = urlParams.get('type') || defaultTypeSort || TYPE_TRENDING;
const timeSort = urlParams.get('time') || TIME_WEEK;
const tagsInUrl = urlParams.get('t') || '';
const options: {
@ -91,8 +87,8 @@ function ClaimListDiscover(props: Props) {
// 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 && !hideCustomization && personalSort === SEARCH_SORT_YOU) || !personalView ? tags : [],
channel_ids: personalSort === SEARCH_SORT_CHANNELS ? subscribedChannels.map(sub => sub.uri.split('#')[1]) : [],
any_tags: tags || [],
channel_ids: channelIds || [],
not_channel_ids: hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
not_tags: !showNsfw ? MATURE_TAGS : [],
order_by:
@ -110,41 +106,17 @@ function ClaimListDiscover(props: Props) {
.unix()
)}`;
}
const hasContent =
(personalSort === SEARCH_SORT_CHANNELS && subscribedChannels.length) ||
(personalSort === SEARCH_SORT_YOU && !!tags.length) ||
personalSort === SEARCH_SORT_ALL;
const hasMatureTags = tags.some(t => MATURE_TAGS.includes(t));
const hasMatureTags = tags && tags.some(t => MATURE_TAGS.includes(t));
const claimSearchCacheQuery = createNormalizedClaimSearchKey(options);
const uris = (hasContent && claimSearchByQuery[claimSearchCacheQuery]) || [];
const uris = claimSearchByQuery[claimSearchCacheQuery] || [];
const shouldPerformSearch =
hasContent &&
(uris.length === 0 ||
didNavigateForward ||
(!loading && uris.length < PAGE_SIZE * page && uris.length % PAGE_SIZE === 0));
uris.length === 0 ||
didNavigateForward ||
(!loading && uris.length < PAGE_SIZE * page && uris.length % PAGE_SIZE === 0);
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
const optionsStringForEffect = JSON.stringify(options);
const noChannels = (
<div>
<p>
<I18nMessage>You're not following any channels.</I18nMessage>
</p>
<p>
<I18nMessage
tokens={{
trending: (
<Button button="link" label={__('trending for everyone')} navigate={'/?type=trending&sort=everyone'} />
),
discover: <Button button="link" label={__('discover some channels!')} navigate={'/$/following'} />,
}}
>
Look what's %trending% or %discover%
</I18nMessage>
</p>
</div>
);
const noResults = (
<div>
<p>
@ -174,22 +146,6 @@ function ClaimListDiscover(props: Props) {
</div>
);
const noTags = (
<p>
<I18nMessage
tokens={{
customize: <Button button="link" navigate={`/$/${PAGES.FOLLOWING}`} label={__('customize')} />,
}}
>
You're not following any tags. Add tags above or smash that %customize% button!
</I18nMessage>
</p>
);
const noFollowing =
(personalSort === SEARCH_SORT_YOU && noTags) || (personalSort === SEARCH_SORT_CHANNELS && noChannels);
const emptyState = !loading && !hasContent ? noFollowing : noResults;
function getSearch() {
let search = `?`;
if (!personalView) {
@ -200,7 +156,7 @@ function ClaimListDiscover(props: Props) {
}
function handleTypeSort(newTypeSort) {
let url = `${getSearch()}type=${newTypeSort}&sort=${personalSort}`;
let url = `${getSearch()}type=${newTypeSort}`;
if (newTypeSort === TYPE_TOP) {
url += `&time=${timeSort}`;
}
@ -209,14 +165,9 @@ function ClaimListDiscover(props: Props) {
history.push(url);
}
function handlePersonalSort(newPersonalSort) {
setPage(1);
history.push(`${getSearch()}type=${typeSort}&sort=${newPersonalSort}`);
}
function handleTimeSort(newTimeSort) {
setPage(1);
history.push(`${getSearch()}type=${typeSort}&sort=${personalSort}&time=${newTimeSort}`);
history.push(`${getSearch()}type=${typeSort}&time=${newTimeSort}`);
}
function handleScrollBottom() {
@ -234,47 +185,19 @@ function ClaimListDiscover(props: Props) {
const header = (
<Fragment>
<FormField
className="claim-list__dropdown"
type="select"
name="trending_sort"
value={typeSort}
onChange={e => handleTypeSort(e.target.value)}
>
{SEARCH_TYPES.map(type => (
<option key={type} value={type}>
{__(toCapitalCase(type))}
</option>
))}
</FormField>
{!hideCustomization && (
<Fragment>
<span className="claim-list__conjuction">{__('for')}</span>
{!personalView && tags && tags.length ? (
tags.map(tag => <Tag key={tag} name={tag} disabled type="large" />)
) : (
<FormField
type="select"
name="trending_overview"
className="claim-list__dropdown"
value={personalSort}
onChange={e => {
handlePersonalSort(e.target.value);
}}
>
{SEARCH_FILTER_TYPES.map(type => (
<option key={type} value={type}>
{type === SEARCH_SORT_ALL
? __('Everyone')
: type === SEARCH_SORT_YOU
? __('Tags You Follow')
: __('Channels You Follow')}
</option>
))}
</FormField>
)}
</Fragment>
)}
{SEARCH_TYPES.map(type => (
<Button
key={type}
button="alt"
onClick={() => handleTypeSort(type)}
className={classnames(`button-toggle button-toggle--${type}`, {
'button-toggle--active': typeSort === type,
})}
icon={toCapitalCase(type)}
label={__(toCapitalCase(type))}
/>
))}
{typeSort === 'top' && (
<FormField
className="claim-list__dropdown"
@ -308,7 +231,7 @@ function ClaimListDiscover(props: Props) {
onScrollBottom={handleScrollBottom}
page={page}
pageSize={PAGE_SIZE}
empty={emptyState}
empty={noResults}
/>
{loading && new Array(PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}

View file

@ -329,4 +329,37 @@ export const icons = {
<line x1="3" y1="18" x2="21" y2="18" />
</g>
),
[ICONS.DISCOVER]: buildIcon(
<g>
<circle cx="12" cy="12" r="10" />
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76" />
</g>
),
[ICONS.TRENDING]: buildIcon(
<g>
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18" />
<polyline points="17 6 23 6 23 12" />
</g>
),
[ICONS.TOP]: buildIcon(
<g>
<line x1="12" y1="20" x2="12" y2="10" />
<line x1="18" y1="20" x2="18" y2="4" />
<line x1="6" y1="20" x2="6" y2="16" />
</g>
),
[ICONS.NEW]: buildIcon(
<g>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</g>
),
[ICONS.INVITE]: buildIcon(
<g>
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</g>
),
};

View file

@ -127,33 +127,42 @@ const Header = (props: Props) => {
<div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}>
{(!IS_WEB || authenticated) && (
<Fragment>
<Menu>
<MenuButton className="header__navigation-item menu__title">{getWalletTitle()}</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.WALLET}`)}>
<Icon aria-hidden icon={ICONS.WALLET} />
{__('Wallet')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.FEATURED} />
{__('Rewards')}
</MenuItem>
</MenuList>
</Menu>
<Button
navigate={`/$/${PAGES.WALLET}`}
className="header__navigation-item menu__title header__navigation-item--balance"
label={getWalletTitle()}
/>
<Menu>
<MenuButton className="header__navigation-item menu__title header__navigation-item--icon">
<Icon size={18} icon={ICONS.ACCOUNT} />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.ACCOUNT}`)}>
<Icon aria-hidden icon={ICONS.OVERVIEW} />
{__('Overview')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publish')}
{__('New Publish')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISHED}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Your Publishes')}
</MenuItem>
{/* @if TARGET='app' */}
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}>
<Icon aria-hidden icon={ICONS.LIBRARY} />
{__('Your Library')}
</MenuItem>
{/* @endif */}
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.FEATURED} />
{__('Rewards')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.INVITE}`)}>
<Icon aria-hidden icon={ICONS.INVITE} />
{__('Invite A Friend')}
</MenuItem>
{authenticated ? (
<MenuItem className="menu__link" onSelect={signOut}>
<Icon aria-hidden icon={ICONS.SIGN_OUT} />

View file

@ -17,14 +17,14 @@ type Props = {
function Page(props: Props) {
const { children, className, authPage = false, authenticated } = props;
const obscureSideBar = IS_WEB ? !authenticated : false;
const obscureSideNavigation = IS_WEB ? !authenticated : false;
return (
<Fragment>
<Header authHeader={authPage} />
<div className={classnames('main-wrapper__inner')}>
<main className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage })}>{children}</main>
{!authPage && <SideNavigation obscureSideBar={obscureSideBar} />}
{!authPage && <SideNavigation obscureSideNavigation={obscureSideNavigation} />}
</div>
</Fragment>
);

View file

@ -5,10 +5,10 @@ import { Route, Redirect, Switch, withRouter } from 'react-router-dom';
import SettingsPage from 'page/settings';
import HelpPage from 'page/help';
import ReportPage from 'page/report';
import AccountPage from 'page/account';
import ShowPage from 'page/show';
import PublishPage from 'page/publish';
import DiscoverPage from 'page/discover';
// import HomePage from 'page/home';
import RewardsPage from 'page/rewards';
import FileListDownloaded from 'page/fileListDownloaded';
import FileListPublished from 'page/fileListPublished';
@ -18,6 +18,9 @@ import SearchPage from 'page/search';
import LibraryPage from 'page/library';
import WalletPage from 'page/wallet';
import TagsPage from 'page/tags';
import TagsFollowingPage from 'page/tagsFollowing';
import ChannelsFollowingPage from 'page/channelsFollowing';
import ChannelsFollowingManagePage from 'page/channelsFollowingManage';
import FollowingPage from 'page/following';
import ListBlockedPage from 'page/listBlocked';
import FourOhFourPage from 'page/fourOhFour';
@ -70,10 +73,13 @@ function AppRouter(props: Props) {
return (
<Switch>
<Route path="/" exact component={DiscoverPage} />
{/* <Route path={`/$/${PAGES.HOME}`} exact component={HomePage} /> */}
<Route path={`/$/${PAGES.DISCOVER}`} exact component={DiscoverPage} />
<Route path={`/$/${PAGES.AUTH}`} exact component={SignInPage} />
<Route path={`/$/${PAGES.TAGS}`} exact component={TagsPage} />
<Route path={`/$/${PAGES.TAGS_FOLLOWING}`} exact component={TagsFollowingPage} />
<Route path={`/$/${PAGES.CHANNELS_FOLLOWING}`} exact component={ChannelsFollowingPage} />
<Route path={`/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`} exact component={ChannelsFollowingManagePage} />
<Route path={`/$/${PAGES.HELP}`} exact component={HelpPage} />
<Route path={`/$/${PAGES.AUTH_VERIFY}`} exact component={SignInVerifyPage} />
<Route path={`/$/${PAGES.SEARCH}`} exact component={SearchPage} />
@ -87,7 +93,6 @@ function AppRouter(props: Props) {
<PrivateRoute {...props} path={`/$/${PAGES.REWARDS}`} component={RewardsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.TRANSACTIONS}`} component={TransactionHistoryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.LIBRARY}`} component={LibraryPage} />
<PrivateRoute {...props} path={`/$/${PAGES.ACCOUNT}`} component={AccountPage} />
<PrivateRoute {...props} path={`/$/${PAGES.FOLLOWING}`} component={FollowingPage} />
<PrivateRoute {...props} path={`/$/${PAGES.BLOCKED}`} component={ListBlockedPage} />
<PrivateRoute {...props} path={`/$/${PAGES.WALLET}`} exact component={WalletPage} />

View file

@ -2,6 +2,7 @@
import * as PAGES from 'constants/pages';
import * as ICONS from 'constants/icons';
import React from 'react';
import { withRouter } from 'react-router';
import Button from 'component/button';
import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext';
@ -11,24 +12,27 @@ type Props = {
subscriptions: Array<Subscription>,
followedTags: Array<Tag>,
email: ?string,
obscureSideBar: boolean,
obscureSideNavigation: boolean,
uploadCount: number,
sticky: boolean,
expanded: boolean,
doSignOut: () => void,
location: { pathname: string },
};
function SideBar(props: Props) {
function SideNavigation(props: Props) {
const {
subscriptions,
followedTags,
obscureSideBar,
obscureSideNavigation,
uploadCount,
doSignOut,
email,
sticky = true,
expanded = false,
location,
} = props;
const { pathname } = location;
const isAuthenticated = Boolean(email);
function buildLink(path, label, icon, onClick) {
@ -49,7 +53,7 @@ function SideBar(props: Props) {
<div>{children}</div>
);
return obscureSideBar ? (
return obscureSideNavigation ? (
<Wrapper>
<div className="card navigation--placeholder">
<div className="wrap">
@ -64,29 +68,13 @@ function SideBar(props: Props) {
<ul className="navigation-links">
{[
{
...buildLink(null, __('Home'), ICONS.HOME),
},
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
{
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
...buildLink(PAGES.CHANNELS_FOLLOWING, __('Following'), ICONS.SUBSCRIBE),
},
{
...buildLink(
PAGES.PUBLISHED,
uploadCount ? (
<span>
{__('Publishes')}
<Spinner type="small" />
</span>
) : (
__('Publishes')
),
ICONS.PUBLISH
),
...buildLink(PAGES.TAGS_FOLLOWING, __('Your Tags'), ICONS.TAG),
},
{
...buildLink(PAGES.DISCOVER, __('All Content'), ICONS.DISCOVER),
},
].map(linkProps => (
<li key={linkProps.navigate}>
@ -96,6 +84,28 @@ function SideBar(props: Props) {
{expanded &&
[
// @if TARGET='app'
{
...buildLink(PAGES.LIBRARY, __('Library'), ICONS.LIBRARY),
},
// @endif
{
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
},
{
...buildLink(
PAGES.PUBLISHED,
uploadCount ? (
<span>
{__('Publishes')}
<Spinner type="small" />
</span>
) : (
__('Publishes')
),
ICONS.PUBLISH
),
},
{
...buildLink(PAGES.WALLET, __('Wallet'), ICONS.WALLET),
},
@ -126,27 +136,20 @@ function SideBar(props: Props) {
</li>
))
)}
<li>
<Button
navigate={`/$/${PAGES.FOLLOWING}`}
label={__('Customize')}
icon={ICONS.EDIT}
className="navigation-link"
activeClass="navigation-link--active"
/>
</li>
</ul>
<section className="navigation-links__inline">
<ul className="navigation-links--small tags--vertical">
{pathname.includes(PAGES.TAGS_FOLLOWING) && (
<ul className="navigation__secondary navigation-links--small tags--vertical">
{followedTags.map(({ name }, key) => (
<li className="navigation-link__wrapper" key={name}>
<Tag navigate={`/$/tags?t${name}`} name={name} />
</li>
))}
</ul>
<ul className="navigation-links--small">
)}
{pathname.includes(PAGES.CHANNELS_FOLLOWING) && (
<ul className="navigation__secondary navigation-links--small">
{subscriptions.map(({ uri, channelName }, index) => (
<li key={uri} className="navigation-link__wrapper">
<Button
@ -158,10 +161,10 @@ function SideBar(props: Props) {
</li>
))}
</ul>
</section>
)}
</nav>
</Wrapper>
);
}
export default SideBar;
export default withRouter(SideNavigation);

View file

@ -78,3 +78,6 @@ export const EYE = 'Eye';
export const EYE_OFF = 'EyeOff';
export const SIGN_OUT = 'SignOut';
export const SIGN_IN = 'SignIn';
export const TRENDING = 'Trending';
export const TOP = 'Top';
export const NEW = 'New';

View file

@ -3,6 +3,7 @@ exports.AUTH_VERIFY = 'verify';
exports.BACKUP = 'backup';
exports.CHANNEL = 'channel';
exports.DISCOVER = 'discover';
exports.HOME = 'home';
exports.DOWNLOADED = 'downloaded';
exports.HELP = 'help';
exports.LIBRARY = 'library';
@ -16,10 +17,13 @@ exports.SEND = 'send';
exports.SETTINGS = 'settings';
exports.SHOW = 'show';
exports.ACCOUNT = 'account';
exports.FOLLOWING = 'following';
exports.SEARCH = 'search';
exports.TRANSACTIONS = 'transactions';
exports.TAGS = 'tags';
exports.TAGS_FOLLOWING = 'following/tags';
exports.CHANNELS_FOLLOWING = 'following/channels';
exports.CHANNELS_FOLLOWING_MANAGE = 'following/channels/manage';
exports.FOLLOWING = 'following';
exports.WALLET = 'wallet';
exports.BLOCKED = 'blocked';
exports.CHANNELS = 'channels';

View file

@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectSubscriptions, selectSuggestedChannels } from 'redux/selectors/subscriptions';
import { doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
import DiscoverPage from './view';
const select = state => ({
subscribedChannels: selectSubscriptions(state),
email: selectUserVerifiedEmail(state),
suggestedSubscriptions: selectSuggestedChannels(state),
});
const perform = {
doFetchRecommendedSubscriptions,
};
export default connect(
select,
perform
)(DiscoverPage);

View file

@ -0,0 +1,56 @@
// @flow
import * as PAGES from 'constants/pages';
import React from 'react';
import ClaimListDiscover from 'component/claimListDiscover';
import ClaimList from 'component/claimList';
import Page from 'component/page';
import Button from 'component/button';
import { TYPE_NEW } from 'component/claimListDiscover/view';
type Props = {
email: string,
subscribedChannels: Array<Subscription>,
doFetchRecommendedSubscriptions: () => void,
suggestedSubscriptions: Array<{ uri: string }>,
};
function ChannelsFollowing(props: Props) {
const { subscribedChannels, suggestedSubscriptions, doFetchRecommendedSubscriptions } = props;
const hasSubsribedChannels = subscribedChannels.length > 0;
const [showTab, setShowTab] = React.useState(!hasSubsribedChannels);
React.useEffect(() => {
if (!hasSubsribedChannels) {
doFetchRecommendedSubscriptions();
}
}, [doFetchRecommendedSubscriptions, hasSubsribedChannels]);
return (
<Page>
{showTab ? (
<div className="card">
<ClaimList
header={__('Find Channels To Follow')}
headerAltControls={
<Button
button="link"
label={hasSubsribedChannels && __('View Your Feed')}
onClick={() => setShowTab(false)}
/>
}
uris={suggestedSubscriptions.map(sub => `lbry://${sub.uri}`)}
/>
</div>
) : (
<ClaimListDiscover
defaultTypeSort={TYPE_NEW}
channelIds={subscribedChannels.map(sub => sub.uri.split('#')[1])}
meta={<Button button="link" label={__('Manage')} navigate={`/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`} />}
/>
)}
</Page>
);
}
export default ChannelsFollowing;

View file

@ -0,0 +1,16 @@
import { connect } from 'react-redux';
import { selectSubscriptions, selectSuggestedChannels } from 'redux/selectors/subscriptions';
import { doFetchRecommendedSubscriptions } from 'redux/actions/subscriptions';
import ChannelsFollowingManagePage from './view';
const select = state => ({
subscribedChannels: selectSubscriptions(state),
suggestedSubscriptions: selectSuggestedChannels(state),
});
export default connect(
select,
{
doFetchRecommendedSubscriptions,
}
)(ChannelsFollowingManagePage);

View file

@ -0,0 +1,57 @@
// @flow
import * as PAGES from 'constants/pages';
import React, { useEffect } from 'react';
import Page from 'component/page';
import ClaimList from 'component/claimList';
import Button from 'component/button';
type Props = {
subscribedChannels: Array<Subscription>,
location: { search: string },
history: { push: string => void },
doFetchRecommendedSubscriptions: () => void,
suggestedSubscriptions: Array<{ uri: string }>,
};
function ChannelsFollowingManagePage(props: Props) {
const { subscribedChannels, location, history, doFetchRecommendedSubscriptions, suggestedSubscriptions } = props;
const hasSubscriptions = !!subscribedChannels.length;
const channelUris = subscribedChannels.map(({ uri }) => uri);
const { search } = location;
const urlParams = new URLSearchParams(search);
const viewingSuggestedSubs = urlParams.get('view') || !hasSubscriptions;
function onClick() {
let url = `/$/${PAGES.CHANNELS_FOLLOWING_MANAGE}`;
if (!viewingSuggestedSubs) {
url += '?view=discover';
}
history.push(url);
}
useEffect(() => {
doFetchRecommendedSubscriptions();
}, [doFetchRecommendedSubscriptions]);
return (
<Page>
<div className="card">
<ClaimList
header={viewingSuggestedSubs ? __('Discover New Channels') : __('Channels You Follow')}
headerAltControls={
<Button
button="link"
label={viewingSuggestedSubs ? hasSubscriptions && __('View Your Channels') : __('Find New Channels')}
onClick={() => onClick()}
/>
}
uris={viewingSuggestedSubs ? suggestedSubscriptions.map(sub => `lbry://${sub.uri}`) : channelUris}
/>
</div>
</Page>
);
}
export default ChannelsFollowingManagePage;

View file

@ -1,28 +1,12 @@
// @flow
import * as PAGES from 'constants/pages';
import React from 'react';
import ClaimListDiscover from 'component/claimListDiscover';
import TagsSelect from 'component/tagsSelect';
import Page from 'component/page';
import Button from 'component/button';
type Props = {
followedTags: Array<Tag>,
email: string,
};
function DiscoverPage(props: Props) {
const { followedTags, email } = props;
function DiscoverPage() {
return (
<Page>
{(email || !IS_WEB) && <TagsSelect showClose title={__('Customize Your Homepage')} />}
<ClaimListDiscover
hideCustomization={IS_WEB && !email}
personalView
tags={followedTags.map(tag => tag.name)}
meta={<Button button="link" label={__('Customize')} requiresAuth={IS_WEB} navigate={`/$/${PAGES.FOLLOWING}`} />}
/>
<ClaimListDiscover />
</Page>
);
}

18
ui/page/home/index.js Normal file
View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import DiscoverPage from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
subscribedChannels: selectSubscriptions(state),
email: selectUserVerifiedEmail(state),
});
const perform = {};
export default connect(
select,
perform
)(DiscoverPage);

22
ui/page/home/view.jsx Normal file
View file

@ -0,0 +1,22 @@
// @flow
import React from 'react';
import Page from 'component/page';
// import Button from 'component/button';
// type Props = {};
function HomePage() {
// props: Props
// const {} = props;
return (
<Page>
{/* TODO */}
{/* Recent From Following */}
{/* Trending for your tags */}
{/* Trending for everyone */}
</Page>
);
}
export default HomePage;

View file

@ -0,0 +1,18 @@
import { connect } from 'react-redux';
import { selectFollowedTags } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'lbryinc';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import DiscoverPage from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
subscribedChannels: selectSubscriptions(state),
email: selectUserVerifiedEmail(state),
});
const perform = {};
export default connect(
select,
perform
)(DiscoverPage);

View file

@ -0,0 +1,30 @@
// @flow
import * as PAGES from 'constants/pages';
import React from 'react';
import ClaimListDiscover from 'component/claimListDiscover';
import TagsSelect from 'component/tagsSelect';
import Page from 'component/page';
import Button from 'component/button';
type Props = {
followedTags: Array<Tag>,
email: string,
};
function DiscoverPage(props: Props) {
const { followedTags, email } = props;
return (
<Page>
{(email || !IS_WEB) && <TagsSelect showClose title={__('Find New Tags To Follow')} />}
<ClaimListDiscover
hideCustomization={IS_WEB && !email}
personalView
tags={followedTags.map(tag => tag.name)}
meta={<Button button="link" label={__('Customize')} requiresAuth={IS_WEB} navigate={`/$/${PAGES.FOLLOWING}`} />}
/>
</Page>
);
}
export default DiscoverPage;

View file

@ -467,6 +467,7 @@ export function doGetAndPopulatePreferences() {
);
});
}
doPreferenceGet('shared', successCb, failCb);
};
}

View file

@ -64,3 +64,42 @@ svg + .button__label,
.button__label + svg {
margin-left: var(--spacing-small);
}
.button-toggle {
padding: 0 var(--spacing-medium);
height: var(--height-button);
font-size: var(--font-base);
border: 1px solid var(--color-border);
border-left-width: 0;
border-radius: 0;
margin: 0;
svg {
opacity: 0.5;
}
&:first-of-type {
border-left-width: 1px;
border-top-left-radius: var(--border-radius);
border-bottom-left-radius: var(--border-radius);
}
&:last-of-type {
border-top-right-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
}
.button-toggle--active {
color: var(--color-primary);
background-color: var(--color-primary-alt);
svg {
opacity: 1;
}
&:hover {
cursor: default;
text-decoration: none;
}
}

View file

@ -7,7 +7,7 @@
.claim-list__header {
display: flex;
align-items: center;
min-height: 4.5rem;
min-height: 6rem;
padding: var(--spacing-medium);
font-size: var(--font-body);
border-bottom: 1px solid var(--color-border);
@ -15,10 +15,6 @@
border-top-left-radius: var(--card-radius);
border-top-right-radius: var(--card-radius);
& > *:not(:last-child) {
margin-right: 0.5rem;
}
fieldset-section {
margin-top: 0;
margin-bottom: 0;
@ -34,6 +30,7 @@
margin-bottom: 0;
padding: 0 var(--spacing-medium);
padding-right: var(--spacing-large);
margin-left: var(--spacing-medium);
@media (max-width: $breakpoint-small) {
font-size: var(--font-small);
@ -42,6 +39,10 @@
}
}
.claim-list__header-title {
display: block;
}
.claim-list__conjuction {
color: var(--color-text-subtitle);
font-size: var(--font-small);

View file

@ -47,6 +47,7 @@
flex: 1;
display: flex;
align-items: center;
height: var(--height-input);
}
.header__menu {
@ -113,7 +114,18 @@
.header__navigation-item--back,
.header__navigation-item--forward,
.header__navigation-item--icon {
width: 3rem;
width: var(--height-button);
background-color: var(--color-header-button);
border-radius: 1.5rem;
margin-left: var(--spacing-small);
svg {
stroke: var(--color-text);
}
&:hover {
background-color: var(--color-primary-alt);
}
}
.header__navigation-item--lbry {
@ -125,3 +137,7 @@
width: 2rem;
}
}
.header__navigation-item--balance {
margin: 0 var(--spacing-medium);
}

View file

@ -13,7 +13,7 @@
.modal {
@extend .card;
background-color: var(--color-card-background);
background-color: var(--color-modal-background);
line-height: 1.55;
min-width: 500px;
max-width: var(--modal-width);

View file

@ -7,6 +7,10 @@
}
}
.navigation__secondary {
margin-top: var(--spacing-large);
}
.navigation--placeholder {
@extend .navigation;
padding: 2rem 1.5rem;

View file

@ -25,8 +25,6 @@
@extend .tags;
flex-direction: column;
align-items: flex-start;
margin: 0;
margin-top: var(--spacing-small);
li:last-child .tag {
margin-bottom: 0;

View file

@ -7,6 +7,7 @@
position: relative;
z-index: 1;
font-size: var(--font-small);
height: var(--height-input);
> .icon {
top: 0;

View file

@ -18,10 +18,6 @@
outline: none;
background-color: var(--color-menu-background);
border-top: none;
&:focus {
box-shadow: none;
}
}
[data-reach-menu-item] {

View file

@ -1,6 +1,13 @@
// Generic html styles used across the App
// component specific styling should go in the component scss file
.top-bid {
width: 100%;
height: 400px;
background-color: var(--color-primary-alt);
padding: 1rem;
}
*::selection {
background-color: var(--color-text-selection-bg);
color: var(--color-text-selection);

View file

@ -5,11 +5,11 @@
/* #5a6570; - 25% */
[theme='dark'] {
--color-primary-alt: #30574e;
--color-primary: #60e1ba;
--color-primary-alt: #3e675d;
--color-primary: #74dfbf;
// Button
--color-link: #38d9a9;
--color-link: var(--color-primary);
--color-link-hover: #60e1ba;
--color-link-active: #60e1ba;
--color-link-icon: #89939e;
@ -21,6 +21,7 @@
--color-button-secondary-bg: #395877;
--color-button-secondary-bg-hover: #4b6d8f;
--color-button-secondary-text: #a3c1e0;
--color-header-button: var(--color-link-icon);
// Color
--color-focus: #2d69a5;
@ -34,6 +35,7 @@
--color-tab-text: var(--color-white);
--color-tabs-background: #434b53;
--color-tab-divider: var(--color-white);
--color-modal-background: var(--color-header-background);
// Text
--color-text: #eeeeee;
@ -49,7 +51,7 @@
--color-input: #f4f4f5;
--color-input-label: #d4d4d4;
--color-input-placeholder: #f4f4f5;
--color-input-bg: #7d8894;
--color-input-bg: #5d6772;
--color-input-bg-copyable: #434b53;
--color-input-border: var(--color-border);
--color-input-border-active: var(--color-secondary);

View file

@ -1,8 +1,9 @@
:root {
// Button
--color-link-icon: var(--color-gray-5);
--color-link-icon: var(--color-gray-4);
--color-link-active: var(--color-primary);
--color-navigation-link: var(--color-link-icon);
--color-navigation-link: var(--color-gray-5);
--color-header-button: #f7f7f7;
// Color
--color-background: #f7f7f7;
@ -34,6 +35,7 @@
--color-file-viewer-background: var(--color-card-background);
--color-tabs-background: var(--color-secondary-alt);
--color-tab-divider: var(--color-secondary);
--color-modal-background: var(--color-card-background);
// Menu
--color-menu-background: var(--color-header-background);