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,
|
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', {
|
||||||
|
|
|
@ -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}
|
||||||
|
|
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 { 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));
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue