show channels + streams as winning claim from search query
This commit is contained in:
parent
73c146b9ac
commit
f2c6986a6f
10 changed files with 195 additions and 58 deletions
|
@ -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', {
|
||||
|
|
|
@ -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}
|
||||
|
|
12
ui/component/searchTopClaim/index.js
Normal file
12
ui/component/searchTopClaim/index.js
Normal 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);
|
90
ui/component/searchTopClaim/view.jsx
Normal file
90
ui/component/searchTopClaim/view.jsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue