diff --git a/static/app-strings.json b/static/app-strings.json
index 6eb8bbf74..385ff366a 100644
--- a/static/app-strings.json
+++ b/static/app-strings.json
@@ -991,4 +991,4 @@
"Email %help_link% or join our %chat_link% if you encounter any trouble verifying.": "Email %help_link% or join our %chat_link% if you encounter any trouble verifying.",
"Show reposts": "Show reposts",
"Show reposts from the creators you follow.": "Show reposts from the creators you follow."
-}
\ No newline at end of file
+}
diff --git a/ui/component/channelContent/view.jsx b/ui/component/channelContent/view.jsx
index 2b6748966..f69fb4637 100644
--- a/ui/component/channelContent/view.jsx
+++ b/ui/component/channelContent/view.jsx
@@ -4,7 +4,7 @@ import HiddenNsfwClaims from 'component/hiddenNsfwClaims';
import { withRouter } from 'react-router-dom';
import Button from 'component/button';
import ClaimListDiscover from 'component/claimListDiscover';
-import { TYPE_NEW } from 'component/claimListDiscover/view';
+import * as CS from 'constants/claim_search';
type Props = {
uri: string,
@@ -51,7 +51,7 @@ function ChannelContent(props: Props) {
{!channelIsMine && claimsInChannel > 0 && }
{claim && claimsInChannel > 0 ? (
-
+
) : (
This channel hasn't published anything yet
)}
diff --git a/ui/component/claimListDiscover/view.jsx b/ui/component/claimListDiscover/view.jsx
index 74b15274c..46bb33ce9 100644
--- a/ui/component/claimListDiscover/view.jsx
+++ b/ui/component/claimListDiscover/view.jsx
@@ -3,6 +3,7 @@ import type { Node } from 'react';
import classnames from 'classnames';
import React, { Fragment, useEffect, useState } from 'react';
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';
@@ -11,26 +12,12 @@ import ClaimList from 'component/claimList';
import ClaimPreview from 'component/claimPreview';
import { toCapitalCase } from 'util/string';
import I18nMessage from 'component/i18nMessage';
-
-const PAGE_SIZE = 20;
-export const TIME_DAY = 'day';
-export const TIME_WEEK = 'week';
-export const TIME_MONTH = 'month';
-export const TIME_YEAR = 'year';
-export const TIME_ALL = 'all';
-
-export const TYPE_TRENDING = 'trending';
-export const TYPE_TOP = 'top';
-export const TYPE_NEW = 'new';
-
-const SEARCH_TYPES = [TYPE_TRENDING, TYPE_NEW, TYPE_TOP];
-const SEARCH_TIMES = [TIME_DAY, TIME_WEEK, TIME_MONTH, TIME_YEAR, TIME_ALL];
+import * as ICONS from 'constants/icons';
type Props = {
uris: Array,
subscribedChannels: Array,
doClaimSearch: ({}) => void,
- tags: Array,
loading: boolean,
personalView: boolean,
doToggleTagFollow: string => void,
@@ -45,17 +32,22 @@ type Props = {
hiddenUris: Array,
hiddenNsfwMessage?: Node,
channelIds?: Array,
- defaultTypeSort?: string,
- defaultTimeSort?: string,
- defaultOrderBy?: Array,
+ tags: Array,
+ orderBy?: Array,
+ defaultOrderBy?: string,
+ freshness?: string,
+ defaultFreshness?: string,
header?: Node,
headerLabel?: string | Node,
name?: string,
- pageSize?: number,
- claimType?: Array,
+ hideBlock?: boolean,
+ claimType?: string | Array,
+ defaultClaimType?: string | Array,
+ streamType?: string | Array,
+ defaultStreamType?: string | Array,
renderProperties?: Claim => Node,
includeSupportAction?: boolean,
- hideBlock: boolean,
+ noCustom?: boolean,
};
function ClaimListDiscover(props: Props) {
@@ -64,7 +56,6 @@ function ClaimListDiscover(props: Props) {
claimSearchByQuery,
tags,
loading,
- personalView,
meta,
channelIds,
showNsfw,
@@ -73,70 +64,93 @@ function ClaimListDiscover(props: Props) {
location,
hiddenUris,
hiddenNsfwMessage,
- defaultTypeSort,
- defaultTimeSort,
defaultOrderBy,
+ orderBy,
headerLabel,
header,
name,
claimType,
pageSize,
+ hideBlock,
+ defaultClaimType,
+ streamType,
+ defaultStreamType,
+ freshness,
+ defaultFreshness,
renderProperties,
includeSupportAction,
- hideBlock,
+ noCustom,
} = props;
const didNavigateForward = history.action === 'PUSH';
- const [page, setPage] = useState(1);
const { search } = location;
+
+ const [page, setPage] = useState(1);
const [forceRefresh, setForceRefresh] = useState();
+ const [expanded, setExpanded] = useState(false);
+
const urlParams = new URLSearchParams(search);
- const typeSort = urlParams.get('type') || defaultTypeSort || TYPE_TRENDING;
- const timeSort = urlParams.get('time') || defaultTimeSort || TIME_WEEK;
- const tagsInUrl = urlParams.get('t') || '';
+ const tagsParam = tags || urlParams.get(CS.TAGS_KEY) || null;
+ const orderParam = orderBy || urlParams.get(CS.ORDER_BY_KEY) || defaultOrderBy || CS.ORDER_BY_TRENDING;
+ const freshnessParam = freshness || urlParams.get(CS.FRESH_KEY) || defaultFreshness || CS.FRESH_WEEK;
+ const contentTypeParam = urlParams.get(CS.CONTENT_KEY);
+ const claimTypeParam =
+ claimType || (CS.CLAIM_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultClaimType || null;
+ const streamTypeParam =
+ streamType || (CS.FILE_TYPES.includes(contentTypeParam) && contentTypeParam) || defaultStreamType || null;
+ const durationParam = urlParams.get(CS.DURATION_KEY) || null;
+
+ const isFiltered = () =>
+ Boolean(urlParams.get(CS.FRESH_KEY) || urlParams.get(CS.CONTENT_KEY) || urlParams.get(CS.DURATION_KEY));
+
+ useEffect(() => {
+ if (isFiltered()) setExpanded(true);
+ }, []);
+
const options: {
page_size: number,
page: number,
no_totals: boolean,
any_tags: Array,
+ not_tags: Array,
channel_ids: Array,
not_channel_ids: Array,
- not_tags: Array,
order_by: Array,
release_time?: string,
claim_type?: Array,
name?: string,
claim_type?: Array,
+ duration?: string,
+ claim_type?: string | Array,
+ stream_types?: any,
} = {
- page_size: pageSize || PAGE_SIZE,
+ page_size: pageSize || CS.PAGE_SIZE,
page,
name,
claim_type: claimType || undefined,
// no_totals makes it so the sdk doesn't have to calculate total number pages for pagination
// it's faster, but we will need to remove it if we start using total_pages
no_totals: true,
- any_tags: tags || [],
+ any_tags: tagsParam || [],
channel_ids: channelIds || [],
not_channel_ids:
// If channelIds were passed in, we don't need not_channel_ids
!channelIds && hiddenUris && hiddenUris.length ? hiddenUris.map(hiddenUri => hiddenUri.split('#')[1]) : [],
not_tags: !showNsfw ? MATURE_TAGS : [],
order_by:
- defaultOrderBy ||
- (typeSort === TYPE_TRENDING
- ? ['trending_group', 'trending_mixed']
- : typeSort === TYPE_NEW
- ? ['release_time']
- : ['effective_amount']), // Sort by top
+ orderParam === CS.ORDER_BY_TRENDING
+ ? CS.ORDER_BY_TRENDING_VALUE
+ : orderParam === CS.ORDER_BY_NEW
+ ? CS.ORDER_BY_NEW_VALUE
+ : CS.ORDER_BY_TOP_VALUE, // Sort by top
};
-
- if (typeSort === TYPE_TOP && timeSort !== TIME_ALL) {
+ if (orderParam === CS.ORDER_BY_TOP && freshnessParam !== CS.FRESH_ALL) {
options.release_time = `>${Math.floor(
moment()
- .subtract(1, timeSort)
+ .subtract(1, freshnessParam)
.startOf('hour')
.unix()
)}`;
- } else if (typeSort === TYPE_NEW || typeSort === TYPE_TRENDING) {
+ } else if (orderParam === CS.ORDER_BY_NEW || orderParam === CS.ORDER_BY_TRENDING) {
// Warning - hack below
// If users are following more than 10 channels or tags, limit results to stuff less than a year old
// For more than 20, drop it down to 6 months
@@ -145,14 +159,14 @@ function ClaimListDiscover(props: Props) {
if (options.channel_ids.length > 20 || options.any_tags.length > 20) {
options.release_time = `>${Math.floor(
moment()
- .subtract(6, TIME_MONTH)
+ .subtract(3, CS.FRESH_MONTH)
.startOf('week')
.unix()
)}`;
} else if (options.channel_ids.length > 10 || options.any_tags.length > 10) {
options.release_time = `>${Math.floor(
moment()
- .subtract(1, TIME_YEAR)
+ .subtract(1, CS.FRESH_YEAR)
.startOf('week')
.unix()
)}`;
@@ -166,6 +180,26 @@ function ClaimListDiscover(props: Props) {
}
}
+ if (durationParam) {
+ if (durationParam === CS.DURATION_SHORT) {
+ options.duration = '<=1800';
+ } else if (durationParam === CS.DURATION_LONG) {
+ options.duration = '>=1800';
+ }
+ }
+
+ if (streamTypeParam) {
+ if (streamTypeParam !== CS.CONTENT_ALL) {
+ options.stream_types = [streamTypeParam];
+ }
+ }
+
+ if (claimTypeParam) {
+ if (claimTypeParam !== CS.CONTENT_ALL) {
+ options.claim_type = [claimTypeParam];
+ }
+ }
+
if (!showReposts) {
options.claim_type =
options.claim_type === undefined
@@ -179,7 +213,7 @@ function ClaimListDiscover(props: Props) {
const shouldPerformSearch =
uris.length === 0 ||
didNavigateForward ||
- (!loading && uris.length < PAGE_SIZE * page && uris.length % PAGE_SIZE === 0);
+ (!loading && uris.length < CS.PAGE_SIZE * page && uris.length % CS.PAGE_SIZE === 0);
// Don't use the query from createNormalizedClaimSearchKey for the effect since that doesn't include page & release_time
const optionsStringForEffect = JSON.stringify(options);
@@ -191,13 +225,13 @@ function ClaimListDiscover(props: Props) {
again: (
@@ -212,28 +246,45 @@ function ClaimListDiscover(props: Props) {
);
- function getSearch() {
- let search = `?`;
- if (!personalView) {
- search += `t=${tagsInUrl}&`;
- }
-
- return search;
- }
-
- function handleTypeSort(newTypeSort) {
- let url = `${getSearch()}type=${newTypeSort}`;
- if (newTypeSort === TYPE_TOP) {
- url += `&time=${timeSort}`;
- }
-
+ function handleChange(change) {
+ const url = buildUrl(change);
setPage(1);
history.push(url);
}
- function handleTimeSort(newTimeSort) {
- setPage(1);
- history.push(`${getSearch()}type=${typeSort}&time=${newTimeSort}`);
+ function buildUrl(delta) {
+ const newUrlParams = new URLSearchParams();
+ 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:
+ 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;
+ }
+ return `?${newUrlParams.toString()}`;
}
function handleScrollBottom() {
@@ -251,39 +302,151 @@ function ClaimListDiscover(props: Props) {
const defaultHeader = (
- {SEARCH_TYPES.map(type => (
-
);
@@ -299,7 +462,7 @@ function ClaimListDiscover(props: Props) {
headerAltControls={meta}
onScrollBottom={handleScrollBottom}
page={page}
- pageSize={PAGE_SIZE}
+ pageSize={CS.PAGE_SIZE}
empty={noResults}
renderProperties={renderProperties}
includeSupportAction={includeSupportAction}
@@ -307,7 +470,7 @@ function ClaimListDiscover(props: Props) {
/>
- {loading && new Array(PAGE_SIZE).fill(1).map((x, i) => )}
+ {loading && new Array(CS.PAGE_SIZE).fill(1).map((x, i) => )}
);
diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx
index 1f4134544..51eec66ba 100644
--- a/ui/component/common/icon-custom.jsx
+++ b/ui/component/common/icon-custom.jsx
@@ -412,4 +412,24 @@ export const icons = {
),
+ [ICONS.SLIDERS]: buildIcon(
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ [ICONS.SCIENCE]: buildIcon(
+
+
+
+
+
+ ),
};
diff --git a/ui/component/userChannelFollowIntro/view.jsx b/ui/component/userChannelFollowIntro/view.jsx
index 7c76b2e36..13cd05e37 100644
--- a/ui/component/userChannelFollowIntro/view.jsx
+++ b/ui/component/userChannelFollowIntro/view.jsx
@@ -1,7 +1,7 @@
// @flow
import React from 'react';
import ClaimListDiscover from 'component/claimListDiscover';
-import { TYPE_TOP, TIME_ALL } from 'component/claimListDiscover/view';
+import * as CS from 'constants/claim_search';
import Nag from 'component/common/nag';
type Props = {
@@ -22,7 +22,12 @@ function UserChannelFollowIntro(props: Props) {
)}
-
+
{followingCount > 0 && (
,
};
@@ -29,7 +28,7 @@ function ChannelsFollowingPage(props: Props) {
{__('Following')}
}
- defaultTypeSort={TYPE_NEW}
+ defaultOrderBy={ORDER_BY_NEW}
channelIds={subscribedChannels.map(sub => sub.uri.split('#')[1])}
meta={
))}
{__('More Channels')}
-
+
);
}
diff --git a/ui/page/top/view.jsx b/ui/page/top/view.jsx
index d0348202a..c17659a50 100644
--- a/ui/page/top/view.jsx
+++ b/ui/page/top/view.jsx
@@ -3,7 +3,7 @@ import React from 'react';
import Page from 'component/page';
import ClaimListDiscover from 'component/claimListDiscover';
import ClaimEffectiveAmount from 'component/claimEffectiveAmount';
-import { TYPE_TOP, TIME_ALL } from 'component/claimListDiscover/view';
+import { ORDER_BY_TOP, FRESH_ALL } from 'constants/claim_search';
type Props = {
name: string,
@@ -16,9 +16,8 @@ function TopPage(props: Props) {
(
diff --git a/ui/scss/all.scss b/ui/scss/all.scss
index 79e84e598..f47c533bc 100644
--- a/ui/scss/all.scss
+++ b/ui/scss/all.scss
@@ -33,6 +33,7 @@
@import 'component/pagination';
@import 'component/placeholder';
@import 'component/search';
+@import 'component/claim-search';
@import 'component/section';
@import 'component/snack-bar';
@import 'component/spinner';
diff --git a/ui/scss/component/_button.scss b/ui/scss/component/_button.scss
index cb0dd94cc..4751a2375 100644
--- a/ui/scss/component/_button.scss
+++ b/ui/scss/component/_button.scss
@@ -78,7 +78,9 @@ svg + .button__label,
border-radius: 0;
margin: 0;
background-color: var(--color-card-background);
-
+ @media (max-width: $breakpoint-small) {
+ padding: var(--spacing-medium) var(--spacing-small);
+ }
svg {
opacity: 0.5;
}
@@ -109,3 +111,10 @@ svg + .button__label,
text-decoration: none;
}
}
+
+.button-toggle--custom {
+ color: var(--color-primary);
+ svg {
+ opacity: 1;
+ }
+}
diff --git a/ui/scss/component/_claim-search.scss b/ui/scss/component/_claim-search.scss
new file mode 100644
index 000000000..9ecec7a9d
--- /dev/null
+++ b/ui/scss/component/_claim-search.scss
@@ -0,0 +1,61 @@
+.claim-search__wrapper {
+ width: 100%;
+}
+
+.claim-search__menus {
+ background-color: var(--color-card-background);
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+ justify-content: flex-start;
+ align-items: center;
+ padding: var(--spacing-medium);
+ padding-bottom: var(--spacing-small);
+ margin-top: var(--spacing-medium);
+ @media (max-width: $breakpoint-small) {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ color: var(--color-text-subtitle);
+}
+
+.claim-search__dropdown {
+ padding: 0 var(--spacing-medium);
+ max-width: 400px;
+ @media (max-width: $breakpoint-small) {
+ margin-left: 0;
+ }
+ background-color: var(--color-card-background);
+ width: var(--option-select-width);
+}
+
+.claim-search__dropdown--selected {
+ background-color: var(--color-primary-alt);
+}
+.claim-search__input-container {
+ &:not(:first-of-type) {
+ padding-left: var(--spacing-medium);
+ }
+ @media (max-width: $breakpoint-small) {
+ &:not(:first-of-type) {
+ margin-top: var(--spacing-small);
+ }
+ padding-left: 0px;
+ &:not(:first-of-type) {
+ padding-left: 0;
+ }
+ }
+}
+
+.claim-search__extra {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+}
+
+.claim-search__top {
+ display: flex;
+ flex-direction: row;
+ flex-wrap: wrap;
+}
diff --git a/ui/scss/init/_vars.scss b/ui/scss/init/_vars.scss
index f70e7148d..10ca9c0d2 100644
--- a/ui/scss/init/_vars.scss
+++ b/ui/scss/init/_vars.scss
@@ -26,6 +26,7 @@ $breakpoint-medium: 1150px;
--floating-viewer-height: 18rem; // 32 * 9/16
--floating-viewer-info-height: 5rem;
--floating-viewer-container-height: calc(var(--floating-viewer-height) + var(--floating-viewer-info-height));
+ --option-select-width: 8rem;
// Text
--text-max-width: 660px;