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

View file

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

View file

@ -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 (
<Page>
<section className="search">
{urlQuery && (
<Fragment>
{isValid && (
<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>
)}
<>
{isValid && <SearchTopClaim query={modifiedUrlQuery} />}
<ClaimList
uris={uris}
@ -135,7 +102,7 @@ export default function SearchPage(props: Props) {
header={!SIMPLE_SITE && <SearchOptions additionalOptions={additionalOptions} />}
injectedItem={SHOW_ADS && !isAuthenticated && IS_WEB && <Ads type="video" />}
headerAltControls={
<Fragment>
<>
<span>{__('Find what you were looking for?')}</span>
<Button
button="alt"
@ -149,12 +116,12 @@ export default function SearchPage(props: Props) {
onClick={() => onFeedbackNegative(urlQuery)}
icon={ICONS.NO}
/>
</Fragment>
</>
}
/>
<div className="help">{__('These search results are provided by LBRY, Inc.')}</div>
</Fragment>
<div className="main--empty help">{__('These search results are provided by LBRY, Inc.')}</div>
</>
)}
</section>
</Page>

View file

@ -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 (
<Page>
<SearchTopClaim query={name} hideLink />
<ClaimListDiscover
name={name}
name={channelActive ? `@${name}` : name}
defaultFreshness={FRESH_ALL}
defaultOrderBy={ORDER_BY_TOP}
includeSupportAction
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} />
</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>
);

View file

@ -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;
}
);
};

View file

@ -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;

View file

@ -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);
}