new layout 🕺

This commit is contained in:
Sean Yesmunt 2020-08-21 11:49:13 -04:00
parent 02d2962004
commit 19fb7d7f06
37 changed files with 728 additions and 484 deletions

View file

@ -70,7 +70,7 @@ type RowDataItem = {
options?: {}, options?: {},
}; };
export default function getHomePageRowData( export default function GetHomePageRowData(
authenticated: boolean, authenticated: boolean,
showPersonalizedChannels: boolean, showPersonalizedChannels: boolean,
showPersonalizedTags: boolean, showPersonalizedTags: boolean,

View file

@ -8,6 +8,7 @@ import Spinner from 'component/spinner';
import { FormField } from 'component/common/form'; import { FormField } from 'component/common/form';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import debounce from 'util/debounce'; import debounce from 'util/debounce';
import ClaimPreviewTile from 'component/claimPreviewTile';
const DEBOUNCE_SCROLL_HANDLER_MS = 150; const DEBOUNCE_SCROLL_HANDLER_MS = 150;
const SORT_NEW = 'new'; const SORT_NEW = 'new';
@ -34,7 +35,7 @@ type Props = {
hideBlock: boolean, hideBlock: boolean,
injectedItem: ?Node, injectedItem: ?Node,
timedOutMessage?: Node, timedOutMessage?: Node,
isCardBody?: boolean, tileLayout?: boolean,
}; };
export default function ClaimList(props: Props) { export default function ClaimList(props: Props) {
@ -57,8 +58,9 @@ export default function ClaimList(props: Props) {
hideBlock, hideBlock,
injectedItem, injectedItem,
timedOutMessage, timedOutMessage,
isCardBody = false, tileLayout = false,
} = props; } = props;
const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW); const [currentSort, setCurrentSort] = usePersistedState(persistedStorageKey, SORT_NEW);
const timedOut = uris === null; const timedOut = uris === null;
const urisLength = (uris && uris.length) || 0; const urisLength = (uris && uris.length) || 0;
@ -89,7 +91,11 @@ export default function ClaimList(props: Props) {
} }
}, [loading, onScrollBottom, urisLength, pageSize, page]); }, [loading, onScrollBottom, urisLength, pageSize, page]);
return ( return tileLayout && !header ? (
<section className="claim-grid">
{urisLength > 0 && uris.map(uri => <ClaimPreviewTile key={uri} uri={uri} />)}
</section>
) : (
<section <section
className={classnames('claim-list', { className={classnames('claim-list', {
'claim-list--small': type === 'small', 'claim-list--small': type === 'small',
@ -124,8 +130,8 @@ export default function ClaimList(props: Props) {
{urisLength > 0 && ( {urisLength > 0 && (
<ul <ul
className={classnames('ul--no-style', { className={classnames('ul--no-style', {
card: !isCardBody, card: !tileLayout,
'claim-list--card-body': isCardBody, 'claim-list--card-body': tileLayout,
})} })}
> >
{sortedUris.map((uri, index) => ( {sortedUris.map((uri, index) => (
@ -154,6 +160,7 @@ export default function ClaimList(props: Props) {
))} ))}
</ul> </ul>
)} )}
{!timedOut && urisLength === 0 && !loading && ( {!timedOut && urisLength === 0 && !loading && (
<div className="empty empty--centered">{empty || __('No results')}</div> <div className="empty empty--centered">{empty || __('No results')}</div>
)} )}

View file

@ -1,20 +1,16 @@
// @flow // @flow
import type { Node } from 'react'; import type { Node } from 'react';
import classnames from 'classnames'; import * as CS from 'constants/claim_search';
import React, { Fragment, useEffect, useState } from 'react'; import React from 'react';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import { withRouter } from 'react-router'; import { withRouter } from 'react-router';
import * as CS from 'constants/claim_search';
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux'; import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
import { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import moment from 'moment'; import moment from 'moment';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import { toCapitalCase } from 'util/string';
import I18nMessage from 'component/i18nMessage'; import I18nMessage from 'component/i18nMessage';
import * as ICONS from 'constants/icons'; import ClaimListHeader from 'component/claimListHeader';
import Card from 'component/common/card';
type Props = { type Props = {
uris: Array<string>, uris: Array<string>,
@ -58,6 +54,7 @@ type Props = {
injectedItem: ?Node, injectedItem: ?Node,
infiniteScroll?: Boolean, infiniteScroll?: Boolean,
feeAmount?: string, feeAmount?: string,
tileLayout: boolean,
}; };
function ClaimListDiscover(props: Props) { function ClaimListDiscover(props: Props) {
@ -98,13 +95,12 @@ function ClaimListDiscover(props: Props) {
injectedItem, injectedItem,
feeAmount, feeAmount,
uris, uris,
tileLayout,
} = props; } = props;
const didNavigateForward = history.action === 'PUSH'; const didNavigateForward = history.action === 'PUSH';
const { search } = location; const { search } = location;
const [page, setPage] = React.useState(1);
const [page, setPage] = useState(1); const [forceRefresh, setForceRefresh] = React.useState();
const [forceRefresh, setForceRefresh] = useState();
const [expanded, setExpanded] = usePersistedState(`expanded-${location.pathname}`, false);
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING); const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING);
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING); const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
const followed = (followedTags && followedTags.map(t => t.name)) || []; const followed = (followedTags && followedTags.map(t => t.name)) || [];
@ -123,22 +119,6 @@ function ClaimListDiscover(props: Props) {
const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY); const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY);
const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds; const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds;
const feeAmountParam = urlParams.get('fee_amount') || feeAmount || CS.FEE_AMOUNT_ANY; const feeAmountParam = urlParams.get('fee_amount') || feeAmount || CS.FEE_AMOUNT_ANY;
const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL);
const isFiltered = () =>
Boolean(
urlParams.get(CS.FRESH_KEY) ||
urlParams.get(CS.CONTENT_KEY) ||
urlParams.get(CS.DURATION_KEY) ||
urlParams.get(CS.TAGS_KEY) ||
urlParams.get(CS.FEE_AMOUNT_KEY)
);
useEffect(() => {
if (history.action !== 'POP' && isFiltered()) {
setExpanded(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy; let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy;
if (!orderParam) { if (!orderParam) {
@ -151,11 +131,11 @@ function ClaimListDiscover(props: Props) {
} }
} }
useEffect(() => { React.useEffect(() => {
setOrderParamUser(orderParam); setOrderParamUser(orderParam);
}, [orderParam]); }, [orderParam]);
useEffect(() => { React.useEffect(() => {
// One-time update to stash the finalized 'orderParam' at entry. // One-time update to stash the finalized 'orderParam' at entry.
if (history.action !== 'POP') { if (history.action !== 'POP') {
setOrderParamEntry(orderParam); setOrderParamEntry(orderParam);
@ -303,7 +283,7 @@ function ClaimListDiscover(props: Props) {
const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery]; const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery];
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery]; const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery];
const [prevOptions, setPrevOptions] = useState(null); const [prevOptions, setPrevOptions] = React.useState(null);
if (!isJustScrollingToNewPage(prevOptions, options)) { if (!isJustScrollingToNewPage(prevOptions, options)) {
// --- New search, or search options changed. // --- New search, or search options changed.
@ -385,21 +365,6 @@ function ClaimListDiscover(props: Props) {
return JSON.stringify(tmpOptions) === JSON.stringify(tmpPrevOptions); return JSON.stringify(tmpOptions) === JSON.stringify(tmpPrevOptions);
} }
function handleChange(change) {
const url = buildUrl(change);
setPage(1);
history.push(url);
}
function handleAdvancedReset() {
const newUrlParams = new URLSearchParams(search);
newUrlParams.delete('claim_type');
newUrlParams.delete('channel_ids');
const newSearch = `?${newUrlParams.toString()}`;
history.push(newSearch);
}
function getParamFromTags(t) { function getParamFromTags(t) {
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) { if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
return t; return t;
@ -408,69 +373,6 @@ function ClaimListDiscover(props: Props) {
} }
} }
function buildUrl(delta) {
const newUrlParams = new URLSearchParams(location.search);
CS.KEYS.forEach(k => {
// $FlowFixMe append() can't take null as second arg, but get() can return null
if (urlParams.get(k) !== null) newUrlParams.append(k, urlParams.get(k));
});
switch (delta.key) {
case CS.ORDER_BY_KEY:
newUrlParams.set(CS.ORDER_BY_KEY, delta.value);
break;
case CS.FRESH_KEY:
if (delta.value === defaultFreshness || delta.value === CS.FRESH_DEFAULT) {
newUrlParams.delete(CS.FRESH_KEY);
} else {
newUrlParams.set(CS.FRESH_KEY, delta.value);
}
break;
case CS.CONTENT_KEY:
if (delta.value === CS.CLAIM_CHANNEL || delta.value === CS.CLAIM_REPOST) {
newUrlParams.delete(CS.DURATION_KEY);
newUrlParams.set(CS.CONTENT_KEY, delta.value);
} else if (delta.value === CS.CONTENT_ALL) {
newUrlParams.delete(CS.CONTENT_KEY);
} else {
newUrlParams.set(CS.CONTENT_KEY, delta.value);
}
break;
case CS.DURATION_KEY:
if (delta.value === CS.DURATION_ALL) {
newUrlParams.delete(CS.DURATION_KEY);
} else {
newUrlParams.set(CS.DURATION_KEY, delta.value);
}
break;
case CS.TAGS_KEY:
if (delta.value === CS.TAGS_ALL) {
if (defaultTags === CS.TAGS_ALL) {
newUrlParams.delete(CS.TAGS_KEY);
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value);
}
} else if (delta.value === CS.TAGS_FOLLOWED) {
if (defaultTags === CS.TAGS_FOLLOWED) {
newUrlParams.delete(CS.TAGS_KEY);
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value); // redundant but special
}
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value);
}
break;
case CS.FEE_AMOUNT_KEY:
if (delta.value === CS.FEE_AMOUNT_ANY) {
newUrlParams.delete(CS.FEE_AMOUNT_KEY);
} else {
newUrlParams.set(CS.FEE_AMOUNT_KEY, delta.value);
}
break;
}
return `?${newUrlParams.toString()}`;
}
function handleScrollBottom() { function handleScrollBottom() {
if (!loading && infiniteScroll) { if (!loading && infiniteScroll) {
if (claimSearchResult && !claimSearchResultLastPageReached) { if (claimSearchResult && !claimSearchResultLastPageReached) {
@ -479,265 +381,68 @@ function ClaimListDiscover(props: Props) {
} }
} }
useEffect(() => { React.useEffect(() => {
if (shouldPerformSearch) { if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringForEffect); const searchOptions = JSON.parse(optionsStringForEffect);
doClaimSearch(searchOptions); doClaimSearch(searchOptions);
} }
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]); }, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]);
const defaultHeader = repostedClaimId ? null : ( const headerToUse = header || (
<Fragment> <ClaimListHeader
<div className={'claim-search__wrapper'}> channelIds={channelIds}
<div className={'claim-search__top'}> defaultTags={defaultTags}
<div className={'claim-search__top-row'}> tags={tags}
{CS.ORDER_BY_TYPES.map(type => ( freshness={freshness}
<Button defaultFreshness={defaultFreshness}
key={type} claimType={claimType}
button="alt" streamType={streamType}
onClick={e => defaultStreamType={defaultStreamType}
handleChange({ feeAmount={feeAmount}
key: CS.ORDER_BY_KEY, orderBy={orderBy}
value: type, defaultOrderBy={defaultOrderBy}
}) hideFilter={hideFilter}
} hasMatureTags={hasMatureTags}
className={classnames(`button-toggle button-toggle--${type}`, { hiddenNsfwMessage={hiddenNsfwMessage}
'button-toggle--active': orderParam === type, setPage={setPage}
})} tileLayout={tileLayout}
disabled={orderBy}
icon={toCapitalCase(type)}
label={__(toCapitalCase(type))}
/> />
))}
</div>
<div>
{!hideFilter && (
<Button
button={'alt'}
aria-label={__('More')}
className={classnames(`button-toggle button-toggle--top button-toggle--more`, {
'button-toggle--custom': isFiltered(),
})}
icon={ICONS.SLIDERS}
onClick={() => setExpanded(!expanded)}
/>
)}
</div>
</div>
{expanded && (
<>
<div className={classnames('card--inline', `claim-search__menus`)}>
{/* FRESHNESS FIELD */}
{orderParam === CS.ORDER_BY_TOP && (
<div className={'claim-search__input-container'}>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected': freshnessParam !== defaultFreshness,
})}
type="select"
name="trending_time"
label={__('How Fresh')}
value={freshnessParam}
onChange={e =>
handleChange({
key: CS.FRESH_KEY,
value: e.target.value,
})
}
>
{CS.FRESH_TYPES.map(time => (
<option key={time} value={time}>
{/* i18fixme */}
{time === CS.FRESH_DAY && __('Today')}
{time !== CS.FRESH_ALL &&
time !== CS.FRESH_DEFAULT &&
time !== CS.FRESH_DAY &&
__('This ' + toCapitalCase(time)) /* yes, concat before i18n, since it is read from const */}
{time === CS.FRESH_ALL && __('All time')}
{time === CS.FRESH_DEFAULT && __('Default')}
</option>
))}
</FormField>
</div>
)}
{/* CONTENT_TYPES FIELD */}
{!claimType && (
<div
className={classnames('claim-search__input-container', {
'claim-search__input-container--selected': contentTypeParam,
})}
>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected': contentTypeParam,
})}
type="select"
name="claimType"
label={__('Content Type')}
value={contentTypeParam || CS.CONTENT_ALL}
onChange={e =>
handleChange({
key: CS.CONTENT_KEY,
value: e.target.value,
})
}
>
{CS.CONTENT_TYPES.map(type => {
if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIdsParam)) {
return (
<option key={type} value={type}>
{/* i18fixme */}
{type === CS.CLAIM_CHANNEL && __('Channel')}
{type === CS.CLAIM_REPOST && __('Repost')}
{type === CS.FILE_VIDEO && __('Video')}
{type === CS.FILE_AUDIO && __('Audio')}
{type === CS.FILE_IMAGE && __('Image')}
{type === CS.FILE_MODEL && __('Model')}
{type === CS.FILE_BINARY && __('Other')}
{type === CS.FILE_DOCUMENT && __('Document')}
{type === CS.CONTENT_ALL && __('Any')}
</option>
);
}
})}
</FormField>
</div>
)}
{/* DURATIONS FIELD */}
{showDuration && (
<div className={'claim-search__input-container'}>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected': durationParam,
})}
label={__('Duration')}
type="select"
name="duration"
disabled={
!(
contentTypeParam === null ||
streamTypeParam === CS.FILE_AUDIO ||
streamTypeParam === CS.FILE_VIDEO
)
}
value={durationParam || CS.DURATION_ALL}
onChange={e =>
handleChange({
key: CS.DURATION_KEY,
value: e.target.value,
})
}
>
{CS.DURATION_TYPES.map(dur => (
<option key={dur} value={dur}>
{/* i18fixme */}
{dur === CS.DURATION_SHORT && __('Short')}
{dur === CS.DURATION_LONG && __('Long')}
{dur === CS.DURATION_ALL && __('Any')}
</option>
))}
</FormField>
</div>
)}
{/* TAGS FIELD */}
{!tags && (
<div className={'claim-search__input-container'}>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected':
((!defaultTags || defaultTags === CS.TAGS_ALL) && tagsParam && tagsParam !== CS.TAGS_ALL) ||
(defaultTags === CS.TAGS_FOLLOWED && tagsParam !== CS.TAGS_FOLLOWED),
})}
label={__('Tags')}
type="select"
name="tags"
value={tagsParam || CS.TAGS_ALL}
onChange={e =>
handleChange({
key: CS.TAGS_KEY,
value: e.target.value,
})
}
>
{[
CS.TAGS_ALL,
CS.TAGS_FOLLOWED,
...followed,
...(followed.includes(tagsParam) || tagsParam === CS.TAGS_ALL || tagsParam === CS.TAGS_FOLLOWED
? []
: [tagsParam]), // if they unfollow while filtered, add Other
].map(tag => (
<option
key={tag}
value={tag}
className={classnames({
'claim-search__input-special': !followed.includes(tag),
})}
>
{followed.includes(tag) && typeof tag === 'string' && toCapitalCase(tag)}
{tag === CS.TAGS_ALL && __('Any')}
{tag === CS.TAGS_FOLLOWED && __('Following')}
{!followed.includes(tag) && tag !== CS.TAGS_ALL && tag !== CS.TAGS_FOLLOWED && __('Other')}
</option>
))}
</FormField>
</div>
)}
{/* PAID FIELD */}
<div className={'claim-search__input-container'}>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected':
feeAmountParam === CS.FEE_AMOUNT_ONLY_FREE || feeAmountParam === CS.FEE_AMOUNT_ONLY_PAID,
})}
label={__('Price')}
type="select"
name="paidcontent"
value={feeAmountParam}
onChange={e =>
handleChange({
key: CS.FEE_AMOUNT_KEY,
value: e.target.value,
})
}
>
<option value={CS.FEE_AMOUNT_ANY}>{__('Anything')}</option>
<option value={CS.FEE_AMOUNT_ONLY_FREE}>{__('Free')}</option>
<option value={CS.FEE_AMOUNT_ONLY_PAID}>{__('Paid')}</option>
))}
</FormField>
</div>
{channelIdsInUrl && (
<div className={'claim-search__input-container'}>
<label>{__('Advanced Filters from URL')}</label>
<Button button="alt" label={__('Clear')} onClick={handleAdvancedReset} />
</div>
)}
</div>
</>
)}
</div>
{hasMatureTags && hiddenNsfwMessage}
</Fragment>
); );
return ( return (
<React.Fragment> <React.Fragment>
{headerLabel && <label className="claim-list__header-label">{headerLabel}</label>} {headerLabel && <label className="claim-list__header-label">{headerLabel}</label>}
<Card {tileLayout ? (
title={header || defaultHeader} <div>
titleActions={meta && <div className="card__actions--inline">{meta}</div>} {!repostedClaimId && (
isBodyList <div className="section__header--actions">
body={ {headerToUse}
<> {meta && <div className="card__actions--inline">{meta}</div>}
</div>
)}
<ClaimList
tileLayout
id={claimSearchCacheQuery}
loading={loading}
uris={uris || claimSearchResult}
onScrollBottom={handleScrollBottom}
page={page}
pageSize={CS.PAGE_SIZE}
timedOutMessage={timedOutMessage}
renderProperties={renderProperties}
includeSupportAction={includeSupportAction}
hideBlock={hideBlock}
injectedItem={injectedItem}
/>
</div>
) : (
<div>
<div className="section__header--actions">
{headerToUse}
{meta && <div className="card__actions--inline">{meta}</div>}
</div>
<ClaimList <ClaimList
isCardBody
id={claimSearchCacheQuery} id={claimSearchCacheQuery}
loading={loading} loading={loading}
uris={uris || claimSearchResult} uris={uris || claimSearchResult}
@ -752,9 +457,8 @@ function ClaimListDiscover(props: Props) {
/> />
{loading && {loading &&
new Array(pageSize || CS.PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)} new Array(pageSize || CS.PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
</> </div>
} )}
/>
</React.Fragment> </React.Fragment>
); );
} }

View file

@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import { selectFetchingClaimSearch, SETTINGS, selectFollowedTags } from 'lbry-redux';
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doSetClientSetting, doSyncClientSettings } from 'redux/actions/settings';
import ClaimListDiscover from './view';
const select = state => ({
followedTags: selectFollowedTags(state),
loading: selectFetchingClaimSearch(state),
showNsfw: makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state),
});
const perform = {
doToggleTagFollowDesktop,
doSetClientSetting,
doSyncClientSettings,
};
export default connect(select, perform)(ClaimListDiscover);

View file

@ -0,0 +1,459 @@
// @flow
import { SIMPLE_SITE } from 'config';
import * as CS from 'constants/claim_search';
import * as ICONS from 'constants/icons';
import type { Node } from 'react';
import classnames from 'classnames';
import React from 'react';
import usePersistedState from 'effects/use-persisted-state';
import { useHistory } from 'react-router';
import { SETTINGS } from 'lbry-redux';
import { FormField } from 'component/common/form';
import Button from 'component/button';
import { toCapitalCase } from 'util/string';
type Props = {
defaultTags: string,
followedTags?: Array<Tag>,
tags: string,
freshness?: string,
defaultFreshness?: string,
claimType?: Array<string>,
streamType?: string | Array<string>,
defaultStreamType?: string | Array<string>,
feeAmount: string,
orderBy?: Array<string>,
defaultOrderBy?: string,
hideFilter: boolean,
hasMatureTags: boolean,
hiddenNsfwMessage?: Node,
channelIds?: Array<string>,
tileLayout: boolean,
doSetClientSetting: (string, boolean) => void,
setPage: number => void,
doSyncClientSettings: () => void,
};
function ClaimListHeader(props: Props) {
const {
defaultTags,
followedTags,
tags,
freshness,
defaultFreshness,
claimType,
streamType,
defaultStreamType,
feeAmount,
orderBy,
defaultOrderBy,
hideFilter,
hasMatureTags,
hiddenNsfwMessage,
channelIds,
tileLayout,
doSetClientSetting,
doSyncClientSettings,
setPage,
} = props;
const { action, push, location } = useHistory();
const { search } = location;
const [expanded, setExpanded] = usePersistedState(`expanded-${location.pathname}`, false);
const [orderParamEntry, setOrderParamEntry] = usePersistedState(`entry-${location.pathname}`, CS.ORDER_BY_TRENDING);
const [orderParamUser, setOrderParamUser] = usePersistedState(`orderUser-${location.pathname}`, CS.ORDER_BY_TRENDING);
const followed = (followedTags && followedTags.map(t => t.name)) || [];
const urlParams = new URLSearchParams(search);
const tagsParam = // can be 'x,y,z' or 'x' or ['x','y'] or CS.CONSTANT
(tags && getParamFromTags(tags)) ||
(urlParams.get(CS.TAGS_KEY) !== null && urlParams.get(CS.TAGS_KEY)) ||
(defaultTags && getParamFromTags(defaultTags));
const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness;
const contentTypeParam = urlParams.get(CS.CONTENT_KEY);
const streamTypeParam =
streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null;
const durationParam = urlParams.get(CS.DURATION_KEY) || null;
const channelIdsInUrl = urlParams.get(CS.CHANNEL_IDS_KEY);
const channelIdsParam = channelIdsInUrl ? channelIdsInUrl.split(',') : channelIds;
const feeAmountParam = urlParams.get('fee_amount') || feeAmount || CS.FEE_AMOUNT_ANY;
const showDuration = !(claimType && claimType === CS.CLAIM_CHANNEL);
const isFiltered = () =>
Boolean(
urlParams.get(CS.FRESH_KEY) ||
urlParams.get(CS.CONTENT_KEY) ||
urlParams.get(CS.DURATION_KEY) ||
urlParams.get(CS.TAGS_KEY) ||
urlParams.get(CS.FEE_AMOUNT_KEY)
);
React.useEffect(() => {
if (action !== 'POP' && isFiltered()) {
setExpanded(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy;
if (!orderParam) {
if (action === 'POP') {
// Reaching here means user have popped back to the page's entry point (e.g. '/$/tags' without any '?order=').
orderParam = orderParamEntry;
} else {
// This is the direct entry into the page, so we load the user's previous value.
orderParam = orderParamUser;
}
}
React.useEffect(() => {
setOrderParamUser(orderParam);
}, [orderParam]);
React.useEffect(() => {
// One-time update to stash the finalized 'orderParam' at entry.
if (action !== 'POP') {
setOrderParamEntry(orderParam);
}
}, []);
function handleChange(change) {
const url = buildUrl(change);
setPage(1);
push(url);
}
function handleAdvancedReset() {
const newUrlParams = new URLSearchParams(search);
newUrlParams.delete('claim_type');
newUrlParams.delete('channel_ids');
const newSearch = `?${newUrlParams.toString()}`;
push(newSearch);
}
function getParamFromTags(t) {
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
return t;
} else if (Array.isArray(t)) {
return t.join(',');
}
}
function buildUrl(delta) {
const newUrlParams = new URLSearchParams(location.search);
CS.KEYS.forEach(k => {
// $FlowFixMe append() can't take null as second arg, but get() can return null
if (urlParams.get(k) !== null) newUrlParams.append(k, urlParams.get(k));
});
switch (delta.key) {
case CS.ORDER_BY_KEY:
newUrlParams.set(CS.ORDER_BY_KEY, delta.value);
break;
case CS.FRESH_KEY:
if (delta.value === defaultFreshness || delta.value === CS.FRESH_DEFAULT) {
newUrlParams.delete(CS.FRESH_KEY);
} else {
newUrlParams.set(CS.FRESH_KEY, delta.value);
}
break;
case CS.CONTENT_KEY:
if (delta.value === CS.CLAIM_CHANNEL || delta.value === CS.CLAIM_REPOST) {
newUrlParams.delete(CS.DURATION_KEY);
newUrlParams.set(CS.CONTENT_KEY, delta.value);
} else if (delta.value === CS.CONTENT_ALL) {
newUrlParams.delete(CS.CONTENT_KEY);
} else {
newUrlParams.set(CS.CONTENT_KEY, delta.value);
}
break;
case CS.DURATION_KEY:
if (delta.value === CS.DURATION_ALL) {
newUrlParams.delete(CS.DURATION_KEY);
} else {
newUrlParams.set(CS.DURATION_KEY, delta.value);
}
break;
case CS.TAGS_KEY:
if (delta.value === CS.TAGS_ALL) {
if (defaultTags === CS.TAGS_ALL) {
newUrlParams.delete(CS.TAGS_KEY);
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value);
}
} else if (delta.value === CS.TAGS_FOLLOWED) {
if (defaultTags === CS.TAGS_FOLLOWED) {
newUrlParams.delete(CS.TAGS_KEY);
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value); // redundant but special
}
} else {
newUrlParams.set(CS.TAGS_KEY, delta.value);
}
break;
case CS.FEE_AMOUNT_KEY:
if (delta.value === CS.FEE_AMOUNT_ANY) {
newUrlParams.delete(CS.FEE_AMOUNT_KEY);
} else {
newUrlParams.set(CS.FEE_AMOUNT_KEY, delta.value);
}
break;
}
return `?${newUrlParams.toString()}`;
}
return (
<>
<div className="claim-search__wrapper">
<div className="claim-search__top">
<div className="claim-search__top-row">
{CS.ORDER_BY_TYPES.map(type => (
<Button
key={type}
button="alt"
onClick={e =>
handleChange({
key: CS.ORDER_BY_KEY,
value: type,
})
}
className={classnames(`button-toggle button-toggle--${type}`, {
'button-toggle--active': orderParam === type,
})}
disabled={orderBy}
icon={toCapitalCase(type)}
label={__(toCapitalCase(type))}
/>
))}
</div>
<div>
{!hideFilter && !SIMPLE_SITE && (
<Button
button="alt"
aria-label={__('More')}
className={classnames(`button-toggle button-toggle--top button-toggle--more`, {
'button-toggle--custom': isFiltered(),
})}
icon={ICONS.SLIDERS}
onClick={() => setExpanded(!expanded)}
/>
)}
{tileLayout !== undefined && (
<Button
onClick={() => {
doSetClientSetting(SETTINGS.TILE_LAYOUT, !tileLayout);
doSyncClientSettings();
}}
button="alt"
className="button-toggle"
aria-label={tileLayout ? __('Change to list layout') : __('Change to tile layout')}
icon={ICONS.LAYOUT}
/>
)}
</div>
</div>
{expanded && !SIMPLE_SITE && (
<>
<div className={classnames('card--inline', `claim-search__menus`)}>
{/* FRESHNESS FIELD */}
{orderParam === CS.ORDER_BY_TOP && (
<div className="claim-search__input-container">
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected': freshnessParam !== defaultFreshness,
})}
type="select"
name="trending_time"
label={__('How Fresh')}
value={freshnessParam}
onChange={e =>
handleChange({
key: CS.FRESH_KEY,
value: e.target.value,
})
}
>
{CS.FRESH_TYPES.map(time => (
<option key={time} value={time}>
{/* i18fixme */}
{time === CS.FRESH_DAY && __('Today')}
{time !== CS.FRESH_ALL &&
time !== CS.FRESH_DEFAULT &&
time !== CS.FRESH_DAY &&
__('This ' + toCapitalCase(time)) /* yes, concat before i18n, since it is read from const */}
{time === CS.FRESH_ALL && __('All time')}
{time === CS.FRESH_DEFAULT && __('Default')}
</option>
))}
</FormField>
</div>
)}
{/* CONTENT_TYPES FIELD */}
{!claimType && (
<div
className={classnames('claim-search__input-container', {
'claim-search__input-container--selected': contentTypeParam,
})}
>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected': contentTypeParam,
})}
type="select"
name="claimType"
label={__('Content Type')}
value={contentTypeParam || CS.CONTENT_ALL}
onChange={e =>
handleChange({
key: CS.CONTENT_KEY,
value: e.target.value,
})
}
>
{CS.CONTENT_TYPES.map(type => {
if (type !== CS.CLAIM_CHANNEL || (type === CS.CLAIM_CHANNEL && !channelIdsParam)) {
return (
<option key={type} value={type}>
{/* i18fixme */}
{type === CS.CLAIM_CHANNEL && __('Channel')}
{type === CS.CLAIM_REPOST && __('Repost')}
{type === CS.FILE_VIDEO && __('Video')}
{type === CS.FILE_AUDIO && __('Audio')}
{type === CS.FILE_IMAGE && __('Image')}
{type === CS.FILE_MODEL && __('Model')}
{type === CS.FILE_BINARY && __('Other')}
{type === CS.FILE_DOCUMENT && __('Document')}
{type === CS.CONTENT_ALL && __('Any')}
</option>
);
}
})}
</FormField>
</div>
)}
{/* DURATIONS FIELD */}
{showDuration && (
<div className={'claim-search__input-container'}>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected': durationParam,
})}
label={__('Duration')}
type="select"
name="duration"
disabled={
!(
contentTypeParam === null ||
streamTypeParam === CS.FILE_AUDIO ||
streamTypeParam === CS.FILE_VIDEO
)
}
value={durationParam || CS.DURATION_ALL}
onChange={e =>
handleChange({
key: CS.DURATION_KEY,
value: e.target.value,
})
}
>
{CS.DURATION_TYPES.map(dur => (
<option key={dur} value={dur}>
{/* i18fixme */}
{dur === CS.DURATION_SHORT && __('Short')}
{dur === CS.DURATION_LONG && __('Long')}
{dur === CS.DURATION_ALL && __('Any')}
</option>
))}
</FormField>
</div>
)}
{/* TAGS FIELD */}
{!tags && (
<div className={'claim-search__input-container'}>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected':
((!defaultTags || defaultTags === CS.TAGS_ALL) && tagsParam && tagsParam !== CS.TAGS_ALL) ||
(defaultTags === CS.TAGS_FOLLOWED && tagsParam !== CS.TAGS_FOLLOWED),
})}
label={__('Tags')}
type="select"
name="tags"
value={tagsParam || CS.TAGS_ALL}
onChange={e =>
handleChange({
key: CS.TAGS_KEY,
value: e.target.value,
})
}
>
{[
CS.TAGS_ALL,
CS.TAGS_FOLLOWED,
...followed,
...(followed.includes(tagsParam) || tagsParam === CS.TAGS_ALL || tagsParam === CS.TAGS_FOLLOWED
? []
: [tagsParam]), // if they unfollow while filtered, add Other
].map(tag => (
<option
key={tag}
value={tag}
className={classnames({
'claim-search__input-special': !followed.includes(tag),
})}
>
{followed.includes(tag) && typeof tag === 'string' && toCapitalCase(tag)}
{tag === CS.TAGS_ALL && __('Any')}
{tag === CS.TAGS_FOLLOWED && __('Following')}
{!followed.includes(tag) && tag !== CS.TAGS_ALL && tag !== CS.TAGS_FOLLOWED && __('Other')}
</option>
))}
</FormField>
</div>
)}
{/* PAID FIELD */}
<div className={'claim-search__input-container'}>
<FormField
className={classnames('claim-search__dropdown', {
'claim-search__dropdown--selected':
feeAmountParam === CS.FEE_AMOUNT_ONLY_FREE || feeAmountParam === CS.FEE_AMOUNT_ONLY_PAID,
})}
label={__('Price')}
type="select"
name="paidcontent"
value={feeAmountParam}
onChange={e =>
handleChange({
key: CS.FEE_AMOUNT_KEY,
value: e.target.value,
})
}
>
<option value={CS.FEE_AMOUNT_ANY}>{__('Anything')}</option>
<option value={CS.FEE_AMOUNT_ONLY_FREE}>{__('Free')}</option>
<option value={CS.FEE_AMOUNT_ONLY_PAID}>{__('Paid')}</option>
))}
</FormField>
</div>
{channelIdsInUrl && (
<div className={'claim-search__input-container'}>
<label>{__('Advanced Filters from URL')}</label>
<Button button="alt" label={__('Clear')} onClick={handleAdvancedReset} />
</div>
)}
</div>
</>
)}
</div>
{hasMatureTags && hiddenNsfwMessage}
</>
);
}
export default ClaimListHeader;

View file

@ -714,4 +714,11 @@ export const icons = {
<line x1="17.5" y1="15" x2="9" y2="15" /> <line x1="17.5" y1="15" x2="9" y2="15" />
</g> </g>
), ),
[ICONS.LAYOUT]: buildIcon(
<g>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" />
</g>
),
}; };

View file

@ -14,7 +14,13 @@ type Props = {
}; };
export default function NotificationHeaderButton(props: Props) { export default function NotificationHeaderButton(props: Props) {
const { unreadCount, doReadNotifications, user } = props; const {
unreadCount,
// notifications,
// fetching,
doReadNotifications,
user,
} = props;
const notificationsEnabled = user && user.experimental_ui; const notificationsEnabled = user && user.experimental_ui;
const { push } = useHistory(); const { push } = useHistory();

View file

@ -21,10 +21,11 @@ type Props = {
isUpgradeAvailable: boolean, isUpgradeAvailable: boolean,
authPage: boolean, authPage: boolean,
filePage: boolean, filePage: boolean,
homePage: boolean,
noHeader: boolean, noHeader: boolean,
noFooter: boolean, noFooter: boolean,
noSideNavigation: boolean, noSideNavigation: boolean,
fullWidth: boolean, fullWidthPage: boolean,
backout: { backout: {
backLabel?: string, backLabel?: string,
backNavDefault?: string, backNavDefault?: string,
@ -37,12 +38,12 @@ function Page(props: Props) {
const { const {
children, children,
className, className,
authPage = false,
filePage = false, filePage = false,
authPage = false,
fullWidthPage = false,
noHeader = false, noHeader = false,
noFooter = false, noFooter = false,
noSideNavigation = false, noSideNavigation = false,
backout, backout,
} = props; } = props;
const { const {
@ -51,6 +52,7 @@ function Page(props: Props) {
const [sidebarOpen, setSidebarOpen] = usePersistedState('sidebar', true); const [sidebarOpen, setSidebarOpen] = usePersistedState('sidebar', true);
const isMediumScreen = useIsMediumScreen(); const isMediumScreen = useIsMediumScreen();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
let isOnFilePage = false; let isOnFilePage = false;
try { try {
const url = pathname.slice(1).replace(/:/g, '#'); const url = pathname.slice(1).replace(/:/g, '#');
@ -89,7 +91,11 @@ function Page(props: Props) {
/> />
)} )}
<main <main
className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage, 'main--file-page': filePage })} className={classnames(MAIN_CLASS, className, {
'main--full-width': fullWidthPage,
'main--auth-page': authPage,
'main--file-page': filePage,
})}
> >
{children} {children}
</main> </main>

View file

@ -112,3 +112,4 @@ export const OPEN_LOG = 'FilePlus';
export const OPEN_LOG_FOLDER = 'Folder'; export const OPEN_LOG_FOLDER = 'Folder';
export const LBRY_STATUS = 'BarChart'; export const LBRY_STATUS = 'BarChart';
export const NOTIFICATION = 'Bell'; export const NOTIFICATION = 'Bell';
export const LAYOUT = 'Layout';

View file

@ -1,34 +0,0 @@
import { useState, useEffect } from 'react';
// https://usehooks.com/useMedia/
export default function useMedia(queries, values, defaultValue) {
// Array containing a media query list for each query
const mediaQueryLists = queries.map(q => window.matchMedia(q));
// Function that gets value based on matching media query
const getValue = () => {
// Get index of first media query that matches
const index = mediaQueryLists.findIndex(mql => mql.matches);
// Return related value or defaultValue if none
return typeof values[index] !== 'undefined' ? values[index] : defaultValue;
};
// State and setter for matched value
const [value, setValue] = useState(getValue);
useEffect(
() => {
// Event listener callback
// Note: By defining getValue outside of useEffect we ensure that it has ...
// ... current values of hook args (as this hook callback is created once on mount).
const handler = () => setValue(getValue);
// Set a listener for each media query with above handler as callback.
mediaQueryLists.forEach(mql => mql.addListener(handler));
// Remove listeners on cleanup
return () => mediaQueryLists.forEach(mql => mql.removeListener(handler));
},
[] // Empty array ensures effect is only run on mount and unmount
);
return value;
}

View file

@ -1,11 +1,36 @@
import useMedia from './use-media'; // Widths are taken from "ui/scss/init/vars.scss"
import React from 'react';
function useWindowSize() {
const isWindowClient = typeof window === 'object';
const [windowSize, setWindowSize] = React.useState(isWindowClient ? window.innerWidth : undefined);
React.useEffect(() => {
function setSize() {
setWindowSize(window.innerWidth);
}
if (isWindowClient) {
window.addEventListener('resize', setSize);
return () => window.removeEventListener('resize', setSize);
}
}, [isWindowClient, setWindowSize]);
return windowSize;
}
export function useIsMobile() { export function useIsMobile() {
const isMobile = useMedia(['(min-width: 901px)'], [false], true); const windowSize = useWindowSize();
return isMobile; return windowSize < 901;
} }
export function useIsMediumScreen() { export function useIsMediumScreen() {
const isMobile = useMedia(['(min-width: 1151px)'], [false], true); const windowSize = useWindowSize();
return isMobile; return windowSize < 1151;
}
export function useIsLargeScreen() {
const windowSize = useWindowSize();
return windowSize > 1600;
} }

View file

@ -197,6 +197,18 @@ remote.getCurrentWindow().on('leave-full-screen', event => {
document.webkitExitFullscreen(); document.webkitExitFullscreen();
}); });
document.addEventListener('click', event => {
let { target } = event;
while (target && target !== document) {
if (target.matches('a[href^="http"]') || target.matches('a[href^="mailto"]')) {
event.preventDefault();
shell.openExternal(target.href);
return;
}
target = target.parentNode;
}
});
// @endif // @endif
document.addEventListener('dragover', event => { document.addEventListener('dragover', event => {
@ -205,20 +217,6 @@ document.addEventListener('dragover', event => {
document.addEventListener('drop', event => { document.addEventListener('drop', event => {
event.preventDefault(); event.preventDefault();
}); });
document.addEventListener('click', event => {
let { target } = event;
while (target && target !== document) {
if (target.matches('a[href^="http"]') || target.matches('a[href^="mailto"]')) {
// @if TARGET='app'
event.preventDefault();
shell.openExternal(target.href);
return;
// @endif
}
target = target.parentNode;
}
});
function AppWrapper() { function AppWrapper() {
// Splash screen and sdk setup not needed on web // Splash screen and sdk setup not needed on web

View file

@ -12,7 +12,7 @@ type Props = {
function ChannelNew(props: Props) { function ChannelNew(props: Props) {
const { history } = props; const { history } = props;
return ( return (
<Page noSideNavigation backout={{ title: __('Create Channel') }} className="main--auth-page"> <Page noSideNavigation authPage backout={{ title: __('Create Channel') }}>
<ChannelEdit onDone={() => history.push(`/$/${PAGES.CHANNELS}`)} /> <ChannelEdit onDone={() => history.push(`/$/${PAGES.CHANNELS}`)} />
</Page> </Page>
); );

View file

@ -45,7 +45,7 @@ export default function ChannelsPage(props: Props) {
</> </>
} }
isBodyList isBodyList
body={<ClaimList isCardBody loading={fetchingChannels} uris={channelUrls} />} body={<ClaimList loading={fetchingChannels} uris={channelUrls} />}
/> />
)} )}
</div> </div>

View file

@ -1,9 +1,13 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SETTINGS } from 'lbry-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions'; import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import ChannelsFollowingPage from './view'; import ChannelsFollowingPage from './view';
const select = state => ({ const select = state => ({
subscribedChannels: selectSubscriptions(state), subscribedChannels: selectSubscriptions(state),
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
}); });
export default connect(select)(ChannelsFollowingPage); export default connect(select)(ChannelsFollowingPage);

View file

@ -11,17 +11,19 @@ import Icon from 'component/common/icon';
type Props = { type Props = {
subscribedChannels: Array<Subscription>, subscribedChannels: Array<Subscription>,
tileLayout: boolean,
}; };
function ChannelsFollowingPage(props: Props) { function ChannelsFollowingPage(props: Props) {
const { subscribedChannels } = props; const { subscribedChannels, tileLayout } = props;
const hasSubsribedChannels = subscribedChannels.length > 0; const hasSubsribedChannels = subscribedChannels.length > 0;
return !hasSubsribedChannels ? ( return !hasSubsribedChannels ? (
<ChannelsFollowingDiscoverPage /> <ChannelsFollowingDiscoverPage />
) : ( ) : (
<Page noFooter> <Page noFooter fullWidthPage={tileLayout}>
<ClaimListDiscover <ClaimListDiscover
tileLayout={tileLayout}
headerLabel={ headerLabel={
<span> <span>
<Icon icon={ICONS.SUBSCRIBE} size={10} /> <Icon icon={ICONS.SUBSCRIBE} size={10} />

View file

@ -7,7 +7,7 @@ import CreditCards from './credit-card-logos.png';
export default function CheckoutPage() { export default function CheckoutPage() {
return ( return (
<Page authPage className="main--auth-page"> <Page authPage>
<Card <Card
title={__('Checkout')} title={__('Checkout')}
subtitle={__('Your cart contains 1 item.')} subtitle={__('Your cart contains 1 item.')}

View file

@ -1,8 +1,9 @@
import * as CS from 'constants/claim_search';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { makeSelectClaimForUri, selectFollowedTags, doResolveUri } from 'lbry-redux'; import { makeSelectClaimForUri, selectFollowedTags, doResolveUri, SETTINGS } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { doToggleTagFollowDesktop } from 'redux/actions/tags'; import { doToggleTagFollowDesktop } from 'redux/actions/tags';
import * as CS from 'constants/claim_search'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import Tags from './view'; import Tags from './view';
const select = (state, props) => { const select = (state, props) => {
@ -15,6 +16,7 @@ const select = (state, props) => {
repostedUri: repostedUri, repostedUri: repostedUri,
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null, repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
isAuthenticated: selectUserVerifiedEmail(state), isAuthenticated: selectUserVerifiedEmail(state),
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
}; };
}; };

View file

@ -21,6 +21,7 @@ type Props = {
doToggleTagFollowDesktop: string => void, doToggleTagFollowDesktop: string => void,
doResolveUri: string => void, doResolveUri: string => void,
isAuthenticated: boolean, isAuthenticated: boolean,
tileLayout: boolean,
}; };
function DiscoverPage(props: Props) { function DiscoverPage(props: Props) {
@ -32,6 +33,7 @@ function DiscoverPage(props: Props) {
doToggleTagFollowDesktop, doToggleTagFollowDesktop,
doResolveUri, doResolveUri,
isAuthenticated, isAuthenticated,
tileLayout,
} = props; } = props;
const buttonRef = useRef(); const buttonRef = useRef();
const isHovering = useHover(buttonRef); const isHovering = useHover(buttonRef);
@ -88,8 +90,9 @@ function DiscoverPage(props: Props) {
} }
return ( return (
<Page noFooter> <Page noFooter fullWidthPage={tileLayout}>
<ClaimListDiscover <ClaimListDiscover
tileLayout={tileLayout}
claimType={claimType ? [claimType] : undefined} claimType={claimType ? [claimType] : undefined}
headerLabel={headerLabel} headerLabel={headerLabel}
tags={tags} tags={tags}

View file

@ -116,7 +116,6 @@ function FileListDownloaded(props: Props) {
) : ( ) : (
<div> <div>
<ClaimList <ClaimList
isCardBody
renderProperties={() => null} renderProperties={() => null}
empty={ empty={
viewMode === VIEW_PURCHASES && !query ? ( viewMode === VIEW_PURCHASES && !query ? (

View file

@ -85,7 +85,7 @@ function FileListPublished(props: Props) {
isBodyList isBodyList
body={ body={
<div> <div>
<ClaimList isCardBody loading={fetching} persistedStorageKey="claim-list-published" uris={urls} /> <ClaimList loading={fetching} persistedStorageKey="claim-list-published" uris={urls} />
<Paginate totalPages={urlTotal > 0 ? Math.ceil(urlTotal / Number(pageSize)) : 1} /> <Paginate totalPages={urlTotal > 0 ? Math.ceil(urlTotal / Number(pageSize)) : 1} />
</div> </div>
} }

View file

@ -37,7 +37,7 @@ function HomePage(props: Props) {
); );
return ( return (
<Page> <Page fullWidthPage>
{(authenticated || !IS_WEB) && !subscribedChannels.length && ( {(authenticated || !IS_WEB) && !subscribedChannels.length && (
<div className="notice-message"> <div className="notice-message">
<h1 className="section__title"> <h1 className="section__title">

View file

@ -11,7 +11,7 @@ export default function ReferredPage(props: Props) {
const { fullUri, referrer } = props; const { fullUri, referrer } = props;
return ( return (
<Page authPage className="main--auth-page"> <Page authPage>
<Invited fullUri={fullUri} referrer={referrer} /> <Invited fullUri={fullUri} referrer={referrer} />
</Page> </Page>
); );

View file

@ -17,7 +17,7 @@ function ListBlocked(props: Props) {
<Card <Card
isBodyList isBodyList
title={__('Your Blocked Channels')} title={__('Your Blocked Channels')}
body={<ClaimList isCardBody uris={uris} showUnresolvedClaims showHiddenByUser />} body={<ClaimList uris={uris} showUnresolvedClaims showHiddenByUser />}
/> />
) : ( ) : (
<div className="main--empty"> <div className="main--empty">

View file

@ -5,7 +5,7 @@ import Page from 'component/page';
export default function PasswordResetPage() { export default function PasswordResetPage() {
return ( return (
<Page authPage className="main--auth-page"> <Page authPage>
<UserPasswordReset /> <UserPasswordReset />
</Page> </Page>
); );

View file

@ -5,7 +5,7 @@ import Page from 'component/page';
export default function PasswordSetPage() { export default function PasswordSetPage() {
return ( return (
<Page authPage className="main--auth-page"> <Page authPage>
<UserPasswordSet /> <UserPasswordSet />
</Page> </Page>
); );

View file

@ -5,7 +5,7 @@ import Page from 'component/page';
export default function SignInPage() { export default function SignInPage() {
return ( return (
<Page authPage className="main--auth-page"> <Page authPage>
<UserSignIn /> <UserSignIn />
</Page> </Page>
); );

View file

@ -88,7 +88,7 @@ function SignInVerifyPage(props: Props) {
} }
return ( return (
<Page authPage className="main--auth-page"> <Page authPage>
<div className="main__sign-up"> <div className="main__sign-up">
<Card <Card
title={isAuthenticationSuccess ? __('Sign In Success!') : __('Sign In to lbry.tv')} title={isAuthenticationSuccess ? __('Sign In Success!') : __('Sign In to lbry.tv')}

View file

@ -5,7 +5,7 @@ import Page from 'component/page';
export default function SignUpPage() { export default function SignUpPage() {
return ( return (
<Page authPage className="main--auth-page"> <Page authPage>
<UserSignUp /> <UserSignUp />
</Page> </Page>
); );

View file

@ -8,9 +8,9 @@ import Button from 'component/button';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import * as CS from 'constants/claim_search'; import * as CS from 'constants/claim_search';
function DiscoverPage() { function TagsFollowingPage() {
return ( return (
<Page noFooter> <Page noFooter fullWidthPage>
<ClaimListDiscover <ClaimListDiscover
headerLabel={ headerLabel={
<span> <span>
@ -34,4 +34,4 @@ function DiscoverPage() {
); );
} }
export default DiscoverPage; export default TagsFollowingPage;

View file

@ -5,7 +5,7 @@ import Page from 'component/page';
export default function Welcome() { export default function Welcome() {
return ( return (
<Page noHeader noSideNavigation className="main--auth-page"> <Page noHeader noSideNavigation>
<PrivacyAgreement /> <PrivacyAgreement />
</Page> </Page>
); );

View file

@ -40,6 +40,7 @@ const defaultState = {
[SETTINGS.HIDE_BALANCE]: false, [SETTINGS.HIDE_BALANCE]: false,
[SETTINGS.OS_NOTIFICATIONS_ENABLED]: true, [SETTINGS.OS_NOTIFICATIONS_ENABLED]: true,
[SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: false, [SETTINGS.AUTOMATIC_DARK_MODE_ENABLED]: false,
[SETTINGS.TILE_LAYOUT]: true,
[SETTINGS.DARK_MODE_TIMES]: { [SETTINGS.DARK_MODE_TIMES]: {
from: { hour: '21', min: '00', formattedTime: '21:00' }, from: { hour: '21', min: '00', formattedTime: '21:00' },

View file

@ -323,34 +323,45 @@
} }
.claim-preview--tile { .claim-preview--tile {
$width: calc((100% - var(--spacing-m) * 3) / 4);
width: $width;
@include handleClaimTileGifThumbnail($width);
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);
margin-right: 0; margin-right: 0;
margin-top: 0; margin-top: 0;
margin-left: var(--spacing-m); margin-left: var(--spacing-m);
justify-content: flex-start; justify-content: flex-start;
@media (min-width: $breakpoint-medium) { .media__thumb {
&:first-child, border-bottom-right-radius: 0;
&:nth-child(4n + 1) { border-bottom-left-radius: 0;
margin-left: 0;
}
} }
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
.media__thumb { @media (min-width: $breakpoint-large) {
border-bottom-right-radius: 0; $width: calc((100% - var(--spacing-m) * 5) / 6);
border-bottom-left-radius: 0; width: $width;
@include handleClaimTileGifThumbnail($width);
&:first-child,
&:nth-child(6n + 1) {
margin-left: 0;
}
}
@media (max-width: $breakpoint-large) and (min-width: $breakpoint-medium) {
$width: calc((100% - var(--spacing-m) * 3) / 4);
width: $width;
@include handleClaimTileGifThumbnail($width);
&:first-child,
&:nth-child(4n + 1) {
margin-left: 0;
}
} }
@media (max-width: $breakpoint-medium) and (min-width: $breakpoint-small) { @media (max-width: $breakpoint-medium) and (min-width: $breakpoint-small) {
$width: calc((100vw - var(--side-nav-width--micro) - (var(--spacing-l) * 3)) / 3); $width: calc((100vw - var(--side-nav-width--micro) - var(--spacing-l) * 3) / 3);
width: $width; width: $width;
@include handleClaimTileGifThumbnail($width); @include handleClaimTileGifThumbnail($width);

View file

@ -77,7 +77,7 @@
.claim-search__top > div { .claim-search__top > div {
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
margin: var(--spacing-xxs) 0; margin-bottom: var(--spacing-xxs);
} }
} }

View file

@ -95,7 +95,18 @@
} }
} }
.main--full-width {
@extend .main;
@media (min-width: $breakpoint-large) {
max-width: none;
width: 100%;
padding: 0 var(--spacing-l);
}
}
.main--auth-page { .main--auth-page {
width: 100%;
max-width: 70rem; max-width: 70rem;
margin-top: var(--spacing-main-padding); margin-top: var(--spacing-main-padding);
margin-left: auto; margin-left: auto;
@ -160,10 +171,6 @@
} }
} }
.main--full-width {
width: 100%;
}
.main__sign-in, .main__sign-in,
.main__sign-up { .main__sign-up {
max-width: 27rem; max-width: 27rem;

View file

@ -18,6 +18,13 @@
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);
} }
.section__header--actions {
margin-bottom: var(--spacing-m);
display: flex;
align-items: flex-start;
justify-content: space-between;
}
.section__flex { .section__flex {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;

View file

@ -4,6 +4,7 @@ import * as CS from 'constants/claim_search';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import moment from 'moment'; import moment from 'moment';
import { toCapitalCase } from 'util/string'; import { toCapitalCase } from 'util/string';
import { useIsLargeScreen } from 'effects/use-screensize';
type RowDataItem = { type RowDataItem = {
title: string, title: string,
@ -12,7 +13,7 @@ type RowDataItem = {
options?: {}, options?: {},
}; };
export default function getHomePageRowData( export default function GetHomePageRowData(
authenticated: boolean, authenticated: boolean,
showPersonalizedChannels: boolean, showPersonalizedChannels: boolean,
showPersonalizedTags: boolean, showPersonalizedTags: boolean,
@ -20,6 +21,12 @@ export default function getHomePageRowData(
followedTags: Array<Tag>, followedTags: Array<Tag>,
showIndividualTags: boolean showIndividualTags: boolean
) { ) {
const isLargeScreen = useIsLargeScreen();
function getPageSize(originalSize) {
return isLargeScreen ? originalSize * (3 / 2) : originalSize;
}
let rowData: Array<RowDataItem> = []; let rowData: Array<RowDataItem> = [];
const individualTagDataItems: Array<RowDataItem> = []; const individualTagDataItems: Array<RowDataItem> = [];
const YOUTUBER_CHANNEL_IDS = [ const YOUTUBER_CHANNEL_IDS = [
@ -114,7 +121,7 @@ export default function getHomePageRowData(
options: { options: {
claimType: ['stream'], claimType: ['stream'],
orderBy: ['release_time'], orderBy: ['release_time'],
pageSize: 12, pageSize: getPageSize(12),
channelIds: YOUTUBER_CHANNEL_IDS, channelIds: YOUTUBER_CHANNEL_IDS,
limitClaimsPerChannel: 1, limitClaimsPerChannel: 1,
releaseTime: `>${Math.floor( releaseTime: `>${Math.floor(
@ -160,7 +167,7 @@ export default function getHomePageRowData(
.startOf('week') .startOf('week')
.unix() .unix()
)}`, )}`,
pageSize: subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4, pageSize: getPageSize(subscribedChannels.length > 3 ? (subscribedChannels.length > 6 ? 16 : 8) : 4),
channelIds: subscribedChannels.map((subscription: Subscription) => { channelIds: subscribedChannels.map((subscription: Subscription) => {
const { channelClaimId } = parseURI(subscription.uri); const { channelClaimId } = parseURI(subscription.uri);
return channelClaimId; return channelClaimId;
@ -172,7 +179,7 @@ export default function getHomePageRowData(
title: __('Top Content from Today'), title: __('Top Content from Today'),
link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TOP}&${CS.FRESH_KEY}=${CS.FRESH_DAY}`, link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TOP}&${CS.FRESH_KEY}=${CS.FRESH_DAY}`,
options: { options: {
pageSize: showPersonalizedChannels || showPersonalizedTags ? 4 : 8, pageSize: getPageSize(showPersonalizedChannels || showPersonalizedTags ? 4 : 8),
orderBy: ['effective_amount'], orderBy: ['effective_amount'],
claimType: ['stream'], claimType: ['stream'],
limitClaimsPerChannel: 2, limitClaimsPerChannel: 2,
@ -198,7 +205,7 @@ export default function getHomePageRowData(
title: __('Trending Classics'), title: __('Trending Classics'),
link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TRENDING}&${CS.FRESH_KEY}=${CS.FRESH_WEEK}`, link: `/$/${PAGES.DISCOVER}?${CS.ORDER_BY_KEY}=${CS.ORDER_BY_TRENDING}&${CS.FRESH_KEY}=${CS.FRESH_WEEK}`,
options: { options: {
pageSize: 4, pageSize: getPageSize(4),
claimType: ['stream'], claimType: ['stream'],
limitClaimsPerChannel: 1, limitClaimsPerChannel: 1,
releaseTime: `<${Math.floor( releaseTime: `<${Math.floor(
@ -222,6 +229,7 @@ export default function getHomePageRowData(
title: __('Trending For Your Tags'), title: __('Trending For Your Tags'),
link: `/$/${PAGES.TAGS_FOLLOWING}`, link: `/$/${PAGES.TAGS_FOLLOWING}`,
options: { options: {
pageSize: getPageSize(4),
tags: followedTags.map(tag => tag.name), tags: followedTags.map(tag => tag.name),
claimType: ['stream'], claimType: ['stream'],
limitClaimsPerChannel: 2, limitClaimsPerChannel: 2,
@ -233,7 +241,7 @@ export default function getHomePageRowData(
link: `/@lbry:3f`, link: `/@lbry:3f`,
options: { options: {
orderBy: ['release_time'], orderBy: ['release_time'],
pageSize: 4, pageSize: getPageSize(4),
channelIds: ['3fda836a92faaceedfe398225fb9b2ee2ed1f01a'], channelIds: ['3fda836a92faaceedfe398225fb9b2ee2ed1f01a'],
}, },
}; };
@ -243,7 +251,7 @@ export default function getHomePageRowData(
link: `/@lbrycast:4`, link: `/@lbrycast:4`,
options: { options: {
orderBy: ['release_time'], orderBy: ['release_time'],
pageSize: 4, pageSize: getPageSize(4),
channelIds: ['4c29f8b013adea4d5cca1861fb2161d5089613ea'], channelIds: ['4c29f8b013adea4d5cca1861fb2161d5089613ea'],
}, },
}; };