mirror of
https://github.com/LBRYFoundation/lbry-desktop.git
synced 2025-08-23 17:47:24 +00:00
new layout 🕺
This commit is contained in:
parent
02d2962004
commit
19fb7d7f06
37 changed files with 728 additions and 484 deletions
|
@ -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,
|
||||||
|
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
21
ui/component/claimListHeader/index.js
Normal file
21
ui/component/claimListHeader/index.js
Normal 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);
|
459
ui/component/claimListHeader/view.jsx
Normal file
459
ui/component/claimListHeader/view.jsx
Normal 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;
|
|
@ -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>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
26
ui/index.jsx
26
ui/index.jsx
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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.')}
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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 ? (
|
||||||
|
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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')}
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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' },
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Reference in a new issue