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?: {},
};
export default function getHomePageRowData(
export default function GetHomePageRowData(
authenticated: boolean,
showPersonalizedChannels: boolean,
showPersonalizedTags: boolean,

View file

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

View file

@ -1,20 +1,16 @@
// @flow
import type { Node } from 'react';
import classnames from 'classnames';
import React, { Fragment, useEffect, useState } from 'react';
import * as CS from 'constants/claim_search';
import React from 'react';
import usePersistedState from 'effects/use-persisted-state';
import { withRouter } from 'react-router';
import * as CS from 'constants/claim_search';
import { createNormalizedClaimSearchKey, MATURE_TAGS } from 'lbry-redux';
import { FormField } from 'component/common/form';
import Button from 'component/button';
import moment from 'moment';
import ClaimList from 'component/claimList';
import ClaimPreview from 'component/claimPreview';
import { toCapitalCase } from 'util/string';
import I18nMessage from 'component/i18nMessage';
import * as ICONS from 'constants/icons';
import Card from 'component/common/card';
import ClaimListHeader from 'component/claimListHeader';
type Props = {
uris: Array<string>,
@ -58,6 +54,7 @@ type Props = {
injectedItem: ?Node,
infiniteScroll?: Boolean,
feeAmount?: string,
tileLayout: boolean,
};
function ClaimListDiscover(props: Props) {
@ -98,13 +95,12 @@ function ClaimListDiscover(props: Props) {
injectedItem,
feeAmount,
uris,
tileLayout,
} = props;
const didNavigateForward = history.action === 'PUSH';
const { search } = location;
const [page, setPage] = useState(1);
const [forceRefresh, setForceRefresh] = useState();
const [expanded, setExpanded] = usePersistedState(`expanded-${location.pathname}`, false);
const [page, setPage] = React.useState(1);
const [forceRefresh, setForceRefresh] = React.useState();
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)) || [];
@ -123,22 +119,6 @@ function ClaimListDiscover(props: Props) {
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)
);
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;
if (!orderParam) {
@ -151,11 +131,11 @@ function ClaimListDiscover(props: Props) {
}
}
useEffect(() => {
React.useEffect(() => {
setOrderParamUser(orderParam);
}, [orderParam]);
useEffect(() => {
React.useEffect(() => {
// One-time update to stash the finalized 'orderParam' at entry.
if (history.action !== 'POP') {
setOrderParamEntry(orderParam);
@ -303,7 +283,7 @@ function ClaimListDiscover(props: Props) {
const claimSearchResult = claimSearchByQuery[claimSearchCacheQuery];
const claimSearchResultLastPageReached = claimSearchByQueryLastPageReached[claimSearchCacheQuery];
const [prevOptions, setPrevOptions] = useState(null);
const [prevOptions, setPrevOptions] = React.useState(null);
if (!isJustScrollingToNewPage(prevOptions, options)) {
// --- New search, or search options changed.
@ -385,21 +365,6 @@ function ClaimListDiscover(props: Props) {
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) {
if (t === CS.TAGS_ALL || t === CS.TAGS_FOLLOWED) {
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() {
if (!loading && infiniteScroll) {
if (claimSearchResult && !claimSearchResultLastPageReached) {
@ -479,282 +381,84 @@ function ClaimListDiscover(props: Props) {
}
}
useEffect(() => {
React.useEffect(() => {
if (shouldPerformSearch) {
const searchOptions = JSON.parse(optionsStringForEffect);
doClaimSearch(searchOptions);
}
}, [doClaimSearch, shouldPerformSearch, optionsStringForEffect, forceRefresh]);
const defaultHeader = repostedClaimId ? null : (
<Fragment>
<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 && (
<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>
const headerToUse = header || (
<ClaimListHeader
channelIds={channelIds}
defaultTags={defaultTags}
tags={tags}
freshness={freshness}
defaultFreshness={defaultFreshness}
claimType={claimType}
streamType={streamType}
defaultStreamType={defaultStreamType}
feeAmount={feeAmount}
orderBy={orderBy}
defaultOrderBy={defaultOrderBy}
hideFilter={hideFilter}
hasMatureTags={hasMatureTags}
hiddenNsfwMessage={hiddenNsfwMessage}
setPage={setPage}
tileLayout={tileLayout}
/>
);
return (
<React.Fragment>
{headerLabel && <label className="claim-list__header-label">{headerLabel}</label>}
<Card
title={header || defaultHeader}
titleActions={meta && <div className="card__actions--inline">{meta}</div>}
isBodyList
body={
<>
<ClaimList
isCardBody
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}
/>
{loading &&
new Array(pageSize || CS.PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
</>
}
/>
{tileLayout ? (
<div>
{!repostedClaimId && (
<div className="section__header--actions">
{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
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}
/>
{loading &&
new Array(pageSize || CS.PAGE_SIZE).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
</div>
)}
</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" />
</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) {
const { unreadCount, doReadNotifications, user } = props;
const {
unreadCount,
// notifications,
// fetching,
doReadNotifications,
user,
} = props;
const notificationsEnabled = user && user.experimental_ui;
const { push } = useHistory();

View file

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

View file

@ -112,3 +112,4 @@ export const OPEN_LOG = 'FilePlus';
export const OPEN_LOG_FOLDER = 'Folder';
export const LBRY_STATUS = 'BarChart';
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() {
const isMobile = useMedia(['(min-width: 901px)'], [false], true);
return isMobile;
const windowSize = useWindowSize();
return windowSize < 901;
}
export function useIsMediumScreen() {
const isMobile = useMedia(['(min-width: 1151px)'], [false], true);
return isMobile;
const windowSize = useWindowSize();
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.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
document.addEventListener('dragover', event => {
@ -205,20 +217,6 @@ document.addEventListener('dragover', event => {
document.addEventListener('drop', event => {
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() {
// Splash screen and sdk setup not needed on web

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import CreditCards from './credit-card-logos.png';
export default function CheckoutPage() {
return (
<Page authPage className="main--auth-page">
<Page authPage>
<Card
title={__('Checkout')}
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 { makeSelectClaimForUri, selectFollowedTags, doResolveUri } from 'lbry-redux';
import { makeSelectClaimForUri, selectFollowedTags, doResolveUri, SETTINGS } from 'lbry-redux';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { doToggleTagFollowDesktop } from 'redux/actions/tags';
import * as CS from 'constants/claim_search';
import { makeSelectClientSetting } from 'redux/selectors/settings';
import Tags from './view';
const select = (state, props) => {
@ -15,6 +16,7 @@ const select = (state, props) => {
repostedUri: repostedUri,
repostedClaim: repostedUri ? makeSelectClaimForUri(repostedUri)(state) : null,
isAuthenticated: selectUserVerifiedEmail(state),
tileLayout: makeSelectClientSetting(SETTINGS.TILE_LAYOUT)(state),
};
};

View file

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

View file

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

View file

@ -85,7 +85,7 @@ function FileListPublished(props: Props) {
isBodyList
body={
<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} />
</div>
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -88,7 +88,7 @@ function SignInVerifyPage(props: Props) {
}
return (
<Page authPage className="main--auth-page">
<Page authPage>
<div className="main__sign-up">
<Card
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() {
return (
<Page authPage className="main--auth-page">
<Page authPage>
<UserSignUp />
</Page>
);

View file

@ -8,9 +8,9 @@ import Button from 'component/button';
import Icon from 'component/common/icon';
import * as CS from 'constants/claim_search';
function DiscoverPage() {
function TagsFollowingPage() {
return (
<Page noFooter>
<Page noFooter fullWidthPage>
<ClaimListDiscover
headerLabel={
<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() {
return (
<Page noHeader noSideNavigation className="main--auth-page">
<Page noHeader noSideNavigation>
<PrivacyAgreement />
</Page>
);

View file

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

View file

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

View file

@ -77,7 +77,7 @@
.claim-search__top > div {
@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 {
width: 100%;
max-width: 70rem;
margin-top: var(--spacing-main-padding);
margin-left: auto;
@ -160,10 +171,6 @@
}
}
.main--full-width {
width: 100%;
}
.main__sign-in,
.main__sign-up {
max-width: 27rem;

View file

@ -18,6 +18,13 @@
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 {
display: flex;
align-items: flex-start;

View file

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