show channels + streams as winning claim from search query

This commit is contained in:
Sean Yesmunt 2020-10-28 15:18:58 -04:00
parent 73c146b9ac
commit f2c6986a6f
10 changed files with 195 additions and 58 deletions

View file

@ -62,6 +62,7 @@ type Props = {
hideActions?: boolean, hideActions?: boolean,
renderActions?: Claim => ?Node, renderActions?: Claim => ?Node,
wrapperElement?: string, wrapperElement?: string,
hideRepostLabel?: boolean,
}; };
const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => { const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
@ -97,6 +98,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
hideActions = false, hideActions = false,
renderActions, renderActions,
wrapperElement, wrapperElement,
hideRepostLabel = false,
} = props; } = props;
const WrapperElement = wrapperElement || 'li'; const WrapperElement = wrapperElement || 'li';
const shouldFetch = const shouldFetch =
@ -235,7 +237,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
'claim-preview__wrapper--small': type === 'small', 'claim-preview__wrapper--small': type === 'small',
})} })}
> >
<ClaimRepostAuthor uri={uri} /> {!hideRepostLabel && <ClaimRepostAuthor uri={uri} />}
<div <div
className={classnames('claim-preview', { className={classnames('claim-preview', {

View file

@ -21,6 +21,7 @@ export default function HelpLink(props: Props) {
}} }}
className="icon--help" className="icon--help"
icon={icon || ICONS.HELP} icon={icon || ICONS.HELP}
iconSize={14}
description={description || __('Help')} description={description || __('Help')}
href={href} href={href}
navigate={navigate} navigate={navigate}

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { doResolveUris } from 'lbry-redux';
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
import SearchTopClaim from './view';
const select = (state, props) => ({
winningUri: makeSelectWinningUriForQuery(props.query)(state),
});
export default connect(select, {
doResolveUris,
})(SearchTopClaim);

View file

@ -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<string>) => 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 (
<section className="search">
<header className="search__header">
<div className="claim-preview__actions--header">
<span className="media__uri">
{__('Most supported')}
<HelpLink href="https://lbry.com/faq/tipping" />
</span>
</div>
<div className="card">
<ClaimPreview
hideRepostLabel
uri={winningUri}
type="large"
placeholder="publish"
properties={claim => (
<span className="claim-preview__custom-properties">
<ClaimEffectiveAmount uri={winningUri} />
</span>
)}
/>
</div>
{!hideLink && (
<div className="section__actions--between section__actions--no-margin">
<span />
<Button
button="link"
className="search__top-link"
label={
<I18nMessage tokens={{ name: <strong>{query}</strong> }}>View competing uploads for %name%</I18nMessage>
}
navigate={`/$/${PAGES.TOP}?name=${query}`}
iconRight={ICONS.ARROW_RIGHT}
/>
</div>
)}
</header>
</section>
);
}

View file

@ -1,5 +1,6 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doToast, SETTINGS } from 'lbry-redux'; import { doToast, SETTINGS } from 'lbry-redux';
import { withRouter } from 'react-router';
import { doSearch } from 'redux/actions/search'; import { doSearch } from 'redux/actions/search';
import { selectIsSearching, makeSelectSearchUris, makeSelectQueryWithOptions } from 'redux/selectors/search'; import { selectIsSearching, makeSelectSearchUris, makeSelectQueryWithOptions } from 'redux/selectors/search';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -7,10 +8,12 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user';
import analytics from 'analytics'; import analytics from 'analytics';
import SearchPage from './view'; import SearchPage from './view';
const select = state => { const select = (state, props) => {
const showMature = makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state); const showMature = makeSelectClientSetting(SETTINGS.SHOW_MATURE)(state);
const urlParams = new URLSearchParams(props.location.search);
const urlQuery = urlParams.get('q') || null;
const query = makeSelectQueryWithOptions( const query = makeSelectQueryWithOptions(
null, urlQuery,
showMature === false ? { nsfw: false, isBackgroundSearch: false } : { isBackgroundSearch: false } showMature === false ? { nsfw: false, isBackgroundSearch: false } : { isBackgroundSearch: false }
)(state); )(state);
const uris = makeSelectSearchUris(query)(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));

View file

@ -1,17 +1,14 @@
// @flow // @flow
import { SIMPLE_SITE, SHOW_ADS } from 'config'; import { SIMPLE_SITE, SHOW_ADS } from 'config';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import * as PAGES from 'constants/pages'; import React, { useEffect } from 'react';
import React, { useEffect, Fragment } from 'react';
import { Lbry, regexInvalidURI, parseURI, isNameValid } from 'lbry-redux'; import { Lbry, regexInvalidURI, parseURI, isNameValid } from 'lbry-redux';
import ClaimPreview from 'component/claimPreview';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
import Page from 'component/page'; import Page from 'component/page';
import SearchOptions from 'component/searchOptions'; import SearchOptions from 'component/searchOptions';
import Button from 'component/button'; import Button from 'component/button';
import ClaimUri from 'component/claimUri';
import Ads from 'web/component/ads'; import Ads from 'web/component/ads';
import ClaimEffectiveAmount from 'component/claimEffectiveAmount'; import SearchTopClaim from 'component/searchTopClaim';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb } from 'util/url';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
@ -51,10 +48,16 @@ export default function SearchPage(props: Props) {
} }
const INVALID_URI_CHARS = new RegExp(regexInvalidURI, 'gu'); 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; let isValid = true;
try { try {
({ path, streamName } = parseURI(urlQuery.replace(/ /g, '-').replace(/:/g, '#'))); ({ streamName } = parseURI(uriFromQuery));
if (!isNameValid(streamName)) { if (!isNameValid(streamName)) {
isValid = false; isValid = false;
} }
@ -63,6 +66,7 @@ export default function SearchPage(props: Props) {
} }
let claimId; let claimId;
// Navigate directly to a claim if a claim_id is pasted into the search bar
if (!/\s/.test(urlQuery) && urlQuery.length === 40) { if (!/\s/.test(urlQuery) && urlQuery.length === 40) {
try { try {
const dummyUrlForClaimId = `x#${urlQuery}`; const dummyUrlForClaimId = `x#${urlQuery}`;
@ -77,57 +81,20 @@ export default function SearchPage(props: Props) {
} catch (e) {} } 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); const stringifiedOptions = JSON.stringify(additionalOptions);
useEffect(() => { useEffect(() => {
if (urlQuery) { if (urlQuery) {
const jsonOptions = JSON.parse(stringifiedOptions); const jsonOptions = JSON.parse(stringifiedOptions);
search(urlQuery, jsonOptions); search(urlQuery, jsonOptions);
} }
}, [search, urlQuery]); }, [search, urlQuery, stringifiedOptions]);
return ( return (
<Page> <Page>
<section className="search"> <section className="search">
{urlQuery && ( {urlQuery && (
<Fragment> <>
{isValid && ( {isValid && <SearchTopClaim query={modifiedUrlQuery} />}
<header className="search__header">
<div className="claim-preview__actions--header">
<ClaimUri uri={uriFromQuery} noShortUrl />
<Button
button="link"
className="media__uri--right"
label={__('View top claims for %normalized_uri%', {
normalized_uri: uriFromQuery,
})}
navigate={`/$/${PAGES.TOP}?name=${modifiedUrlQuery}`}
icon={ICONS.TOP}
/>
</div>
<div className="card">
<ClaimPreview
uri={uriFromQuery}
type="large"
placeholder="publish"
properties={claim => (
<span className="claim-preview__custom-properties">
<span className="help--inline">{__('Current winning amount')}</span>
<ClaimEffectiveAmount uri={uriFromQuery} />
</span>
)}
/>
</div>
</header>
)}
<ClaimList <ClaimList
uris={uris} uris={uris}
@ -135,7 +102,7 @@ export default function SearchPage(props: Props) {
header={!SIMPLE_SITE && <SearchOptions additionalOptions={additionalOptions} />} header={!SIMPLE_SITE && <SearchOptions additionalOptions={additionalOptions} />}
injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && <Ads type="video" />} injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && <Ads type="video" />}
headerAltControls={ headerAltControls={
<Fragment> <>
<span>{__('Find what you were looking for?')}</span> <span>{__('Find what you were looking for?')}</span>
<Button <Button
button="alt" button="alt"
@ -149,12 +116,12 @@ export default function SearchPage(props: Props) {
onClick={() => onFeedbackNegative(urlQuery)} onClick={() => onFeedbackNegative(urlQuery)}
icon={ICONS.NO} icon={ICONS.NO}
/> />
</Fragment> </>
} }
/> />
<div className="help">{__('These search results are provided by LBRY, Inc.')}</div> <div className="main--empty help">{__('These search results are provided by LBRY, Inc.')}</div>
</Fragment> </>
)} )}
</section> </section>
</Page> </Page>

View file

@ -1,9 +1,12 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import classnames from 'classnames';
import Page from 'component/page'; import Page from 'component/page';
import ClaimListDiscover from 'component/claimListDiscover'; import ClaimListDiscover from 'component/claimListDiscover';
import ClaimEffectiveAmount from 'component/claimEffectiveAmount'; import ClaimEffectiveAmount from 'component/claimEffectiveAmount';
import SearchTopClaim from 'component/searchTopClaim';
import { ORDER_BY_TOP, FRESH_ALL } from 'constants/claim_search'; import { ORDER_BY_TOP, FRESH_ALL } from 'constants/claim_search';
import Button from 'component/button';
type Props = { type Props = {
name: string, name: string,
@ -11,20 +14,42 @@ type Props = {
function TopPage(props: Props) { function TopPage(props: Props) {
const { name } = props; const { name } = props;
const [channelActive, setChannelActive] = React.useState(false);
return ( return (
<Page> <Page>
<SearchTopClaim query={name} hideLink />
<ClaimListDiscover <ClaimListDiscover
name={name} name={channelActive ? `@${name}` : name}
defaultFreshness={FRESH_ALL} defaultFreshness={FRESH_ALL}
defaultOrderBy={ORDER_BY_TOP} defaultOrderBy={ORDER_BY_TOP}
includeSupportAction includeSupportAction
renderProperties={claim => ( renderProperties={claim => (
<span className="media__subtitle"> <span className="claim-preview__custom-properties">
{claim.meta.is_controlling && <span className="help--inline">{__('Currently winning')}</span>}
<ClaimEffectiveAmount uri={claim.repost_url || claim.canonical_url} /> <ClaimEffectiveAmount uri={claim.repost_url || claim.canonical_url} />
</span> </span>
)} )}
header={<span>{__('Top claims at lbry://%name%', { name })}</span>} header={
<div className="claim-search__top-row">
<Button
label={name}
button="alt"
onClick={() => setChannelActive(false)}
className={classnames('button-toggle', {
'button-toggle--active': !channelActive,
})}
/>
<Button
label={`@${name}`}
button="alt"
onClick={() => setChannelActive(true)}
className={classnames('button-toggle', {
'button-toggle--active': channelActive,
})}
/>
</div>
}
/> />
</Page> </Page>
); );

View file

@ -174,3 +174,33 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
return recommendedContent; 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;
}
);
};

View file

@ -18,8 +18,10 @@
.media__uri { .media__uri {
position: absolute; position: absolute;
transform: translateY(-130%); transform: translateY(-130%);
display: flex;
font-size: var(--font-xsmall); font-size: var(--font-xsmall);
color: var(--color-text-subtitle); color: var(--color-text-subtitle);
font-weight: var(--font-weight-base);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
position: static; position: static;

View file

@ -1,4 +1,5 @@
.search__header { .search__header {
width: 100%;
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);
.placeholder { .placeholder {
@ -29,3 +30,7 @@
} }
} }
} }
.search__top-link {
font-weight: var(--font-weight-body);
}