From f2c6986a6f579b8e45f590714132f57ed24ead95 Mon Sep 17 00:00:00 2001 From: Sean Yesmunt Date: Wed, 28 Oct 2020 15:18:58 -0400 Subject: [PATCH] show channels + streams as winning claim from search query --- ui/component/claimPreview/view.jsx | 4 +- ui/component/common/help-link.jsx | 1 + ui/component/searchTopClaim/index.js | 12 ++++ ui/component/searchTopClaim/view.jsx | 90 ++++++++++++++++++++++++++++ ui/page/search/index.js | 9 ++- ui/page/search/view.jsx | 69 ++++++--------------- ui/page/top/view.jsx | 31 +++++++++- ui/redux/selectors/search.js | 30 ++++++++++ ui/scss/component/_media.scss | 2 + ui/scss/component/_search.scss | 5 ++ 10 files changed, 195 insertions(+), 58 deletions(-) create mode 100644 ui/component/searchTopClaim/index.js create mode 100644 ui/component/searchTopClaim/view.jsx diff --git a/ui/component/claimPreview/view.jsx b/ui/component/claimPreview/view.jsx index 83d2c5e24..e0873f9bc 100644 --- a/ui/component/claimPreview/view.jsx +++ b/ui/component/claimPreview/view.jsx @@ -62,6 +62,7 @@ type Props = { hideActions?: boolean, renderActions?: Claim => ?Node, wrapperElement?: string, + hideRepostLabel?: boolean, }; const ClaimPreview = forwardRef((props: Props, ref: any) => { @@ -97,6 +98,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { hideActions = false, renderActions, wrapperElement, + hideRepostLabel = false, } = props; const WrapperElement = wrapperElement || 'li'; const shouldFetch = @@ -235,7 +237,7 @@ const ClaimPreview = forwardRef((props: Props, ref: any) => { 'claim-preview__wrapper--small': type === 'small', })} > - + {!hideRepostLabel && }
({ + winningUri: makeSelectWinningUriForQuery(props.query)(state), +}); + +export default connect(select, { + doResolveUris, +})(SearchTopClaim); diff --git a/ui/component/searchTopClaim/view.jsx b/ui/component/searchTopClaim/view.jsx new file mode 100644 index 000000000..3319879e6 --- /dev/null +++ b/ui/component/searchTopClaim/view.jsx @@ -0,0 +1,90 @@ +// @flow +import * as ICONS from 'constants/icons'; +import * as PAGES from 'constants/pages'; +import React from 'react'; +import { parseURI } from 'lbry-redux'; +import ClaimPreview from 'component/claimPreview'; +import Button from 'component/button'; +import ClaimEffectiveAmount from 'component/claimEffectiveAmount'; +import HelpLink from 'component/common/help-link'; +import I18nMessage from 'component/i18nMessage'; + +type Props = { + query: string, + winningUri: ?Claim, + doResolveUris: (Array) => void, + hideLink?: boolean, +}; + +export default function SearchTopClaim(props: Props) { + const { doResolveUris, query = '', winningUri, hideLink = false } = props; + const uriFromQuery = `lbry://${query}`; + + let channelUriFromQuery; + try { + const { isChannel } = parseURI(uriFromQuery); + + if (!isChannel) { + channelUriFromQuery = `lbry://@${query}`; + } + } catch (e) {} + + React.useEffect(() => { + let urisToResolve = []; + if (uriFromQuery) { + urisToResolve.push(uriFromQuery); + } + + if (channelUriFromQuery) { + urisToResolve.push(channelUriFromQuery); + } + + if (urisToResolve.length > 0) { + doResolveUris(urisToResolve); + } + }, [doResolveUris, uriFromQuery, channelUriFromQuery]); + + if (!winningUri) { + return null; + } + + return ( +
+
+
+ + {__('Most supported')} + + +
+
+ ( + + + + )} + /> +
+ {!hideLink && ( +
+ +
+ )} +
+
+ ); +} diff --git a/ui/page/search/index.js b/ui/page/search/index.js index e9ae20ae6..77ea557b5 100644 --- a/ui/page/search/index.js +++ b/ui/page/search/index.js @@ -1,5 +1,6 @@ import { connect } from 'react-redux'; import { doToast, SETTINGS } from 'lbry-redux'; +import { withRouter } from 'react-router'; import { doSearch } from 'redux/actions/search'; import { selectIsSearching, makeSelectSearchUris, makeSelectQueryWithOptions } from 'redux/selectors/search'; import { makeSelectClientSetting } from 'redux/selectors/settings'; @@ -7,10 +8,12 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user'; import analytics from 'analytics'; import SearchPage from './view'; -const select = state => { +const select = (state, props) => { const showMature = makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state); + const urlParams = new URLSearchParams(props.location.search); + const urlQuery = urlParams.get('q') || null; const query = makeSelectQueryWithOptions( - null, + urlQuery, showMature === false ? { nsfw: false, isBackgroundSearch: false } : { isBackgroundSearch: false } )(state); const uris = makeSelectSearchUris(query)(state); @@ -45,4 +48,4 @@ const perform = dispatch => ({ }, }); -export default connect(select, perform)(SearchPage); +export default withRouter(connect(select, perform)(SearchPage)); diff --git a/ui/page/search/view.jsx b/ui/page/search/view.jsx index b5a820ccf..8effda082 100644 --- a/ui/page/search/view.jsx +++ b/ui/page/search/view.jsx @@ -1,17 +1,14 @@ // @flow import { SIMPLE_SITE, SHOW_ADS } from 'config'; import * as ICONS from 'constants/icons'; -import * as PAGES from 'constants/pages'; -import React, { useEffect, Fragment } from 'react'; +import React, { useEffect } from 'react'; import { Lbry, regexInvalidURI, parseURI, isNameValid } from 'lbry-redux'; -import ClaimPreview from 'component/claimPreview'; import ClaimList from 'component/claimList'; import Page from 'component/page'; import SearchOptions from 'component/searchOptions'; import Button from 'component/button'; -import ClaimUri from 'component/claimUri'; import Ads from 'web/component/ads'; -import ClaimEffectiveAmount from 'component/claimEffectiveAmount'; +import SearchTopClaim from 'component/searchTopClaim'; import { formatLbryUrlForWeb } from 'util/url'; import { useHistory } from 'react-router'; @@ -51,10 +48,16 @@ export default function SearchPage(props: Props) { } const INVALID_URI_CHARS = new RegExp(regexInvalidURI, 'gu'); - let path, streamName; + const modifiedUrlQuery = urlQuery + .trim() + .replace(/\s+/g, '') + .replace(INVALID_URI_CHARS, ''); + const uriFromQuery = `lbry://${modifiedUrlQuery}`; + + let streamName; let isValid = true; try { - ({ path, streamName } = parseURI(urlQuery.replace(/ /g, '-').replace(/:/g, '#'))); + ({ streamName } = parseURI(uriFromQuery)); if (!isNameValid(streamName)) { isValid = false; } @@ -63,6 +66,7 @@ export default function SearchPage(props: Props) { } let claimId; + // Navigate directly to a claim if a claim_id is pasted into the search bar if (!/\s/.test(urlQuery) && urlQuery.length === 40) { try { const dummyUrlForClaimId = `x#${urlQuery}`; @@ -77,57 +81,20 @@ export default function SearchPage(props: Props) { } catch (e) {} } - const modifiedUrlQuery = - isValid && path - ? path - : urlQuery - .trim() - .replace(/\s+/g, '-') - .replace(INVALID_URI_CHARS, ''); - const uriFromQuery = `lbry://${modifiedUrlQuery}`; - const stringifiedOptions = JSON.stringify(additionalOptions); useEffect(() => { if (urlQuery) { const jsonOptions = JSON.parse(stringifiedOptions); search(urlQuery, jsonOptions); } - }, [search, urlQuery]); + }, [search, urlQuery, stringifiedOptions]); return (
{urlQuery && ( - - {isValid && ( -
-
- -
-
- ( - - {__('Current winning amount')} - - - )} - /> -
-
- )} + <> + {isValid && } } injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && } headerAltControls={ - + <> {__('Find what you were looking for?')}
diff --git a/ui/page/top/view.jsx b/ui/page/top/view.jsx index c17659a50..77eb572fa 100644 --- a/ui/page/top/view.jsx +++ b/ui/page/top/view.jsx @@ -1,9 +1,12 @@ // @flow import React from 'react'; +import classnames from 'classnames'; import Page from 'component/page'; import ClaimListDiscover from 'component/claimListDiscover'; import ClaimEffectiveAmount from 'component/claimEffectiveAmount'; +import SearchTopClaim from 'component/searchTopClaim'; import { ORDER_BY_TOP, FRESH_ALL } from 'constants/claim_search'; +import Button from 'component/button'; type Props = { name: string, @@ -11,20 +14,42 @@ type Props = { function TopPage(props: Props) { const { name } = props; + const [channelActive, setChannelActive] = React.useState(false); return ( + ( - + + {claim.meta.is_controlling && {__('Currently winning')}} )} - header={{__('Top claims at lbry://%name%', { name })}} + header={ +
+
+ } />
); diff --git a/ui/redux/selectors/search.js b/ui/redux/selectors/search.js index dd4037e28..8a61f3846 100644 --- a/ui/redux/selectors/search.js +++ b/ui/redux/selectors/search.js @@ -174,3 +174,33 @@ export const makeSelectRecommendedContentForUri = (uri: string) => return recommendedContent; } ); + +export const makeSelectWinningUriForQuery = (query: string) => { + const uriFromQuery = `lbry://${query}`; + + let channelUriFromQuery; + try { + const { isChannel } = parseURI(uriFromQuery); + if (!isChannel) { + channelUriFromQuery = `lbry://@${query}`; + } + } catch (e) {} + + return createSelector( + makeSelectClaimForUri(uriFromQuery), + makeSelectClaimForUri(channelUriFromQuery), + (claim1, claim2) => { + if (!claim1 && !claim2) { + return undefined; + } else if (!claim1 && claim2) { + return claim2.canonical_url; + } else if (claim1 && !claim2) { + return claim1.canonical_url; + } + + const effectiveAmount1 = claim1 && claim1.meta.effective_amount; + const effectiveAmount2 = claim2 && claim2.meta.effective_amount; + return effectiveAmount1 > effectiveAmount2 ? claim1.canonical_url : claim2.canonical_url; + } + ); +}; diff --git a/ui/scss/component/_media.scss b/ui/scss/component/_media.scss index 378769e1b..c8afa98ab 100644 --- a/ui/scss/component/_media.scss +++ b/ui/scss/component/_media.scss @@ -18,8 +18,10 @@ .media__uri { position: absolute; transform: translateY(-130%); + display: flex; font-size: var(--font-xsmall); color: var(--color-text-subtitle); + font-weight: var(--font-weight-base); @media (max-width: $breakpoint-small) { position: static; diff --git a/ui/scss/component/_search.scss b/ui/scss/component/_search.scss index 3e6ffa493..8f2158e50 100644 --- a/ui/scss/component/_search.scss +++ b/ui/scss/component/_search.scss @@ -1,4 +1,5 @@ .search__header { + width: 100%; margin-bottom: var(--spacing-l); .placeholder { @@ -29,3 +30,7 @@ } } } + +.search__top-link { + font-weight: var(--font-weight-body); +}