Merge pull request #5167 from lbryio/repostFromSearchRebase
New repost flow
This commit is contained in:
commit
bb4cb337ec
33 changed files with 940 additions and 78 deletions
|
@ -139,7 +139,7 @@
|
|||
"imagesloaded": "^4.1.4",
|
||||
"json-loader": "^0.5.4",
|
||||
"lbry-format": "https://github.com/lbryio/lbry-format.git",
|
||||
"lbry-redux": "lbryio/lbry-redux#92a4263c908f6df5727c379a3b3348935891d806",
|
||||
"lbry-redux": "lbryio/lbry-redux#664df6237d777d7cce31e58479bc98563ea98cb9",
|
||||
"lbryinc": "lbryio/lbryinc#eee2cb730ecec95a1344a755035755b0d4dad5cf",
|
||||
"lint-staged": "^7.0.2",
|
||||
"localforage": "^1.7.1",
|
||||
|
|
|
@ -1527,6 +1527,9 @@
|
|||
"Explore": "Explore",
|
||||
"There is a bug... somewhere": "There is a bug... somewhere",
|
||||
"Try refreshing to fix the issue. If that doesn't work, email help@lbry.com for support.": "Try refreshing to fix the issue. If that doesn't work, email help@lbry.com for support.",
|
||||
"No Results": "No Results",
|
||||
"Content preview": "Content preview",
|
||||
"Repost url": "Repost url",
|
||||
"Close sidebar - hide channels you are following": "Close sidebar - hide channels you are following",
|
||||
"Expand sidebar - view channels you are following.": "Expand sidebar - view channels you are following.",
|
||||
"--end--": "--end--"
|
||||
|
|
|
@ -14,7 +14,7 @@ function ClaimEffectiveAmount(props: Props) {
|
|||
return null;
|
||||
}
|
||||
|
||||
return <CreditAmount amount={Number(claim.meta.effective_amount)} />;
|
||||
return <CreditAmount amount={Number(claim.meta.effective_amount || claim.amount)} />;
|
||||
}
|
||||
|
||||
export default ClaimEffectiveAmount;
|
||||
|
|
30
ui/component/claimPreview/claim-preview-loading.jsx
Normal file
30
ui/component/claimPreview/claim-preview-loading.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
isChannel: boolean,
|
||||
type: string,
|
||||
};
|
||||
|
||||
function ClaimPreviewLoading(props: Props) {
|
||||
const { isChannel, type } = props;
|
||||
return (
|
||||
<li
|
||||
className={classnames('claim-preview__wrapper', {
|
||||
'claim-preview__wrapper--channel': isChannel && type !== 'inline',
|
||||
'claim-preview__wrapper--inline': type === 'inline',
|
||||
})}
|
||||
>
|
||||
<div className={classnames('claim-preview', { 'claim-preview--large': type === 'large' })}>
|
||||
<div className="placeholder media__thumb" />
|
||||
<div className="placeholder__wrapper">
|
||||
<div className="placeholder claim-preview__title" />
|
||||
<div className="placeholder media__subtitle" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaimPreviewLoading;
|
32
ui/component/claimPreview/claim-preview-no-content.jsx
Normal file
32
ui/component/claimPreview/claim-preview-no-content.jsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
// @flow
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import Empty from 'component/common/empty';
|
||||
|
||||
type Props = {
|
||||
isChannel: boolean,
|
||||
type: string,
|
||||
};
|
||||
|
||||
function ClaimPreviewNoContent(props: Props) {
|
||||
const { isChannel, type } = props;
|
||||
return (
|
||||
<li
|
||||
className={classnames('claim-preview__wrapper', {
|
||||
'claim-preview__wrapper--channel': isChannel && type !== 'inline',
|
||||
'claim-preview__wrapper--inline': type === 'inline',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classnames('claim-preview', {
|
||||
'claim-preview--large': type === 'large',
|
||||
'claim-preview__empty': true,
|
||||
})}
|
||||
>
|
||||
<Empty text={__('Nothing found here. Like big tech ethics.')} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaimPreviewNoContent;
|
35
ui/component/claimPreview/claim-preview-no-mature.jsx
Normal file
35
ui/component/claimPreview/claim-preview-no-mature.jsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
// @flow
|
||||
import classnames from 'classnames';
|
||||
import React from 'react';
|
||||
import Empty from 'component/common/empty';
|
||||
|
||||
type Props = {
|
||||
isChannel: boolean,
|
||||
type: string,
|
||||
};
|
||||
|
||||
function ClaimPreviewNoMature(props: Props) {
|
||||
const { isChannel, type } = props;
|
||||
return (
|
||||
<li
|
||||
className={classnames('claim-preview__wrapper', {
|
||||
'claim-preview__wrapper--channel': isChannel && type !== 'inline',
|
||||
'claim-preview__wrapper--inline': type === 'inline',
|
||||
})}
|
||||
>
|
||||
<div className={classnames('claim-preview', { 'claim-preview--large': type === 'large' })}>
|
||||
<div className="media__thumb" />
|
||||
<div
|
||||
className={classnames('claim-preview', {
|
||||
'claim-preview--large': type === 'large',
|
||||
'claim-preview__empty': true,
|
||||
})}
|
||||
>
|
||||
<Empty text={__('Mature content hidden by your preferences')} />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default ClaimPreviewNoMature;
|
|
@ -22,6 +22,9 @@ import ClaimRepostAuthor from 'component/claimRepostAuthor';
|
|||
import FileDownloadLink from 'component/fileDownloadLink';
|
||||
import AbandonedChannelPreview from 'component/abandonedChannelPreview';
|
||||
import PublishPending from 'component/publishPending';
|
||||
import ClaimPreviewLoading from './claim-preview-loading';
|
||||
import ClaimPreviewNoMature from './claim-preview-no-mature';
|
||||
import ClaimPreviewNoContent from './claim-preview-no-content';
|
||||
|
||||
type Props = {
|
||||
uri: string,
|
||||
|
@ -58,6 +61,7 @@ type Props = {
|
|||
getFile: string => void,
|
||||
customShouldHide?: Claim => boolean,
|
||||
showUnresolvedClaim?: boolean,
|
||||
showNullPlaceholder?: boolean,
|
||||
includeSupportAction?: boolean,
|
||||
hideActions?: boolean,
|
||||
renderActions?: Claim => ?Node,
|
||||
|
@ -94,6 +98,7 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
streamingUrl,
|
||||
customShouldHide,
|
||||
showUnresolvedClaim,
|
||||
showNullPlaceholder,
|
||||
includeSupportAction,
|
||||
hideActions = false,
|
||||
renderActions,
|
||||
|
@ -195,29 +200,22 @@ const ClaimPreview = forwardRef<any, {}>((props: Props, ref: any) => {
|
|||
}
|
||||
}, [isValid, isResolvingUri, uri, resolveUri, shouldFetch]);
|
||||
|
||||
if (shouldHide) {
|
||||
if (shouldHide && !showNullPlaceholder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (placeholder === 'loading' || claim === undefined || (isResolvingUri && !claim)) {
|
||||
return (
|
||||
<li
|
||||
disabled
|
||||
className={classnames('claim-preview__wrapper', {
|
||||
'claim-preview__wrapper--channel': isChannel && type !== 'inline',
|
||||
'claim-preview__wrapper--inline': type === 'inline',
|
||||
})}
|
||||
>
|
||||
<div className={classnames('claim-preview', { 'claim-preview--large': type === 'large' })}>
|
||||
<div className="placeholder media__thumb" />
|
||||
<div className="placeholder__wrapper">
|
||||
<div className="placeholder claim-preview__title" />
|
||||
<div className="placeholder media__subtitle" />
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
if (placeholder === 'loading' || (isResolvingUri && !claim)) {
|
||||
return <ClaimPreviewLoading isChannel={isChannel} type={type} />;
|
||||
}
|
||||
|
||||
if (claim && showNullPlaceholder && shouldHide && nsfw) {
|
||||
return <ClaimPreviewNoMature isChannel={isChannel} type={type} />;
|
||||
}
|
||||
|
||||
if (!claim && showNullPlaceholder) {
|
||||
return <ClaimPreviewNoContent isChannel={isChannel} type={type} />;
|
||||
}
|
||||
|
||||
if (!shouldFetch && showUnresolvedClaim && !isResolvingUri && claim === null) {
|
||||
return <AbandonedChannelPreview uri={uri} type />;
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import CommentCreate from 'component/commentCreate';
|
|||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import { ENABLE_COMMENT_REACTIONS } from 'config';
|
||||
import { sortComments } from 'util/comments';
|
||||
import Empty from 'component/common/empty';
|
||||
|
||||
type Props = {
|
||||
comments: Array<Comment>,
|
||||
|
@ -58,7 +59,7 @@ function CommentList(props: Props) {
|
|||
Boolean(reactionsById) || !ENABLE_COMMENT_REACTIONS
|
||||
);
|
||||
const linkedCommentId = linkedComment && linkedComment.comment_id;
|
||||
const hasNoComments = totalComments === 0;
|
||||
const hasNoComments = !totalComments;
|
||||
const moreBelow = totalComments - end > 0;
|
||||
const isMyComment = (channelId: string): boolean => {
|
||||
if (myChannels != null && channelId != null) {
|
||||
|
@ -210,7 +211,7 @@ function CommentList(props: Props) {
|
|||
<>
|
||||
<CommentCreate uri={uri} />
|
||||
|
||||
{!isFetchingComments && hasNoComments && <div className="main--empty">{__('Be the first to comment!')}</div>}
|
||||
{!isFetchingComments && hasNoComments && <Empty text={__('That was pretty deep. What do you think?')} />}
|
||||
|
||||
<ul className="comments" ref={commentRef}>
|
||||
{comments &&
|
||||
|
|
30
ui/component/common/empty.jsx
Normal file
30
ui/component/common/empty.jsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
|
||||
type Props = {
|
||||
text: ?string,
|
||||
};
|
||||
|
||||
class Empty extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
text: '',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { text } = this.props;
|
||||
|
||||
return (
|
||||
<div className="empty__wrap">
|
||||
<div>
|
||||
{text && (
|
||||
<div className="empty__content">
|
||||
<p className="empty__text">{text}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Empty;
|
|
@ -74,7 +74,7 @@ function FileActions(props: Props) {
|
|||
push(`/$/${PAGES.CHANNEL_NEW}?redirect=${pathname}`);
|
||||
doToast({ message: __('A channel is required to repost on %SITE_NAME%', { SITE_NAME }) });
|
||||
} else {
|
||||
openModal(MODALS.REPOST, { uri });
|
||||
push(`/$/${PAGES.REPOST_NEW}?from=${encodeURIComponent(uri)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ const Header = (props: Props) => {
|
|||
icon={ICONS.ARROW_LEFT}
|
||||
/>
|
||||
{backTitle && <h1 className="header__auth-title">{isMobile ? simpleBackTitle || backTitle : backTitle}</h1>}
|
||||
{authenticated ? (
|
||||
{authenticated || !IS_WEB ? (
|
||||
<Button
|
||||
aria-label={__('Your wallet')}
|
||||
navigate={`/$/${PAGES.WALLET}`}
|
||||
|
|
45
ui/component/repostCreate/index.js
Normal file
45
ui/component/repostCreate/index.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doHideModal } from 'redux/actions/app';
|
||||
import {
|
||||
makeSelectClaimForUri,
|
||||
makeSelectTitleForUri,
|
||||
selectBalance,
|
||||
selectMyChannelClaims,
|
||||
doRepost,
|
||||
selectRepostError,
|
||||
selectRepostLoading,
|
||||
doClearRepostError,
|
||||
selectMyClaimsWithoutChannels,
|
||||
doCheckPublishNameAvailability,
|
||||
doCheckPendingClaims,
|
||||
makeSelectEffectiveAmountForUri,
|
||||
makeSelectIsUriResolving,
|
||||
} from 'lbry-redux';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import RepostCreate from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
channels: selectMyChannelClaims(state),
|
||||
claim: makeSelectClaimForUri(props.uri)(state),
|
||||
passedRepostClaim: makeSelectClaimForUri(props.name)(state),
|
||||
passedRepostAmount: makeSelectEffectiveAmountForUri(props.name)(state),
|
||||
enteredContentClaim: makeSelectClaimForUri(props.contentUri)(state),
|
||||
enteredRepostClaim: makeSelectClaimForUri(props.repostUri)(state),
|
||||
enteredRepostAmount: makeSelectEffectiveAmountForUri(props.repostUri)(state),
|
||||
title: makeSelectTitleForUri(props.uri)(state),
|
||||
balance: selectBalance(state),
|
||||
error: selectRepostError(state),
|
||||
reposting: selectRepostLoading(state),
|
||||
myClaims: selectMyClaimsWithoutChannels(state),
|
||||
isResolvingPassedRepost: props.name && makeSelectIsUriResolving(`lbry://${props.name}`)(state),
|
||||
isResolvingEnteredRepost: props.repostUri && makeSelectIsUriResolving(`lbry://${props.repostUri}`)(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doHideModal,
|
||||
doRepost,
|
||||
doClearRepostError,
|
||||
doToast,
|
||||
doCheckPublishNameAvailability,
|
||||
doCheckPendingClaims,
|
||||
})(RepostCreate);
|
415
ui/component/repostCreate/view.jsx
Normal file
415
ui/component/repostCreate/view.jsx
Normal file
|
@ -0,0 +1,415 @@
|
|||
// @flow
|
||||
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { CHANNEL_NEW, MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import Card from 'component/common/card';
|
||||
import Button from 'component/button';
|
||||
import SelectChannel from 'component/selectChannel';
|
||||
import { FormField } from 'component/common/form';
|
||||
import { parseURI, isNameValid, creditsToString, isURIValid, normalizeURI } from 'lbry-redux';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import analytics from 'analytics';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
import { URL as SITE_URL, URL_LOCAL, URL_DEV } from 'config';
|
||||
import HelpLink from 'component/common/help-link';
|
||||
|
||||
type Props = {
|
||||
doToast: ({ message: string }) => void,
|
||||
doClearRepostError: () => void,
|
||||
doRepost: StreamRepostOptions => Promise<*>,
|
||||
title: string,
|
||||
claim?: StreamClaim,
|
||||
enteredContentClaim?: StreamClaim,
|
||||
balance: number,
|
||||
channels: ?Array<ChannelClaim>,
|
||||
doCheckPublishNameAvailability: string => Promise<*>,
|
||||
error: ?string,
|
||||
reposting: boolean,
|
||||
uri: string,
|
||||
name: string,
|
||||
contentUri: string,
|
||||
setRepostUri: string => void,
|
||||
setContentUri: string => void,
|
||||
doCheckPendingClaims: () => void,
|
||||
redirectUri?: string,
|
||||
passedRepostAmount: number,
|
||||
enteredRepostAmount: number,
|
||||
isResolvingPassedRepost: boolean,
|
||||
isResolvingEnteredRepost: boolean,
|
||||
};
|
||||
|
||||
function RepostCreate(props: Props) {
|
||||
const {
|
||||
redirectUri,
|
||||
doToast,
|
||||
doClearRepostError,
|
||||
doRepost,
|
||||
claim,
|
||||
enteredContentClaim,
|
||||
balance,
|
||||
channels,
|
||||
reposting,
|
||||
doCheckPublishNameAvailability,
|
||||
uri, // ?from
|
||||
name, // ?to
|
||||
contentUri,
|
||||
setRepostUri,
|
||||
setContentUri,
|
||||
doCheckPendingClaims,
|
||||
enteredRepostAmount,
|
||||
passedRepostAmount,
|
||||
isResolvingPassedRepost,
|
||||
isResolvingEnteredRepost,
|
||||
} = props;
|
||||
|
||||
const defaultName = name || (claim && claim.name) || '';
|
||||
const contentClaimId = claim && claim.claim_id;
|
||||
const enteredClaimId = enteredContentClaim && enteredContentClaim.claim_id;
|
||||
const [repostChannel, setRepostChannel] = usePersistedState('repost-channel', 'anonymous');
|
||||
|
||||
const [repostBid, setRepostBid] = React.useState(0.01);
|
||||
const [repostBidError, setRepostBidError] = React.useState(undefined);
|
||||
const [takeoverAmount, setTakeoverAmount] = React.useState(0);
|
||||
const [enteredRepostName, setEnteredRepostName] = React.useState(defaultName);
|
||||
const [available, setAvailable] = React.useState(true);
|
||||
const [enteredContent, setEnteredContentUri] = React.useState(undefined);
|
||||
const [contentError, setContentError] = React.useState('');
|
||||
|
||||
const { replace, goBack } = useHistory();
|
||||
const resolvingRepost = isResolvingEnteredRepost || isResolvingPassedRepost;
|
||||
const repostUrlName = `lbry://${
|
||||
!repostChannel || repostChannel === CHANNEL_NEW || repostChannel === 'anonymous' ? '' : `${repostChannel}/`
|
||||
}`;
|
||||
|
||||
const contentFirstRender = React.useRef(true);
|
||||
const setAutoRepostBid = amount => {
|
||||
if (balance > amount) {
|
||||
if (amount > 5) {
|
||||
setRepostBid(Number(amount.toFixed(2)));
|
||||
} else {
|
||||
setRepostBid(5);
|
||||
}
|
||||
} else if (balance) {
|
||||
setRepostBid(0.01);
|
||||
}
|
||||
};
|
||||
|
||||
function getSearchUri(value) {
|
||||
const WEB_DEV_PREFIX = `${URL_DEV}/`;
|
||||
const WEB_LOCAL_PREFIX = `${URL_LOCAL}/`;
|
||||
const WEB_PROD_PREFIX = `${SITE_URL}/`;
|
||||
const ODYSEE_PREFIX = `https://odysee.com/`;
|
||||
const includesLbryTvProd = value.includes(WEB_PROD_PREFIX);
|
||||
const includesOdysee = value.includes(ODYSEE_PREFIX);
|
||||
const includesLbryTvLocal = value.includes(WEB_LOCAL_PREFIX);
|
||||
const includesLbryTvDev = value.includes(WEB_DEV_PREFIX);
|
||||
const wasCopiedFromWeb = includesLbryTvDev || includesLbryTvLocal || includesLbryTvProd || includesOdysee;
|
||||
const isLbryUrl = value.startsWith('lbry://') && value !== 'lbry://';
|
||||
const error = '';
|
||||
|
||||
const addLbryIfNot = term => {
|
||||
return term.startsWith('lbry://') ? term : `lbry://${term}`;
|
||||
};
|
||||
if (wasCopiedFromWeb) {
|
||||
let prefix = WEB_PROD_PREFIX;
|
||||
if (includesLbryTvLocal) prefix = WEB_LOCAL_PREFIX;
|
||||
if (includesLbryTvDev) prefix = WEB_DEV_PREFIX;
|
||||
if (includesOdysee) prefix = ODYSEE_PREFIX;
|
||||
|
||||
let query = (value && value.slice(prefix.length).replace(/:/g, '#')) || '';
|
||||
try {
|
||||
const lbryUrl = `lbry://${query}`;
|
||||
parseURI(lbryUrl);
|
||||
return [lbryUrl, null];
|
||||
} catch (e) {
|
||||
return [query, 'error'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!isLbryUrl) {
|
||||
if (value.startsWith('@')) {
|
||||
if (isNameValid(value.slice(1))) {
|
||||
return [value, null];
|
||||
} else {
|
||||
return [value, error];
|
||||
}
|
||||
}
|
||||
return [addLbryIfNot(value), null];
|
||||
} else {
|
||||
try {
|
||||
const isValid = isURIValid(value);
|
||||
if (isValid) {
|
||||
let uri;
|
||||
try {
|
||||
uri = normalizeURI(value);
|
||||
} catch (e) {
|
||||
return [value, null];
|
||||
}
|
||||
return [uri, null];
|
||||
} else {
|
||||
return [value, null];
|
||||
}
|
||||
} catch (e) {
|
||||
return [value, 'error'];
|
||||
}
|
||||
}
|
||||
}
|
||||
// repostName
|
||||
let repostNameError;
|
||||
if (!enteredRepostName) {
|
||||
repostNameError = __('A name is required');
|
||||
} else if (!isNameValid(enteredRepostName, false)) {
|
||||
repostNameError = INVALID_NAME_ERROR;
|
||||
} else if (!available) {
|
||||
repostNameError = __('You already have a claim with this name.');
|
||||
}
|
||||
|
||||
// contentName
|
||||
let contentNameError;
|
||||
if (!enteredContent && enteredContent !== undefined) {
|
||||
contentNameError = __('A name is required');
|
||||
}
|
||||
|
||||
// repostChannel
|
||||
const channelStrings = channels && channels.map(channel => channel.permanent_url).join(',');
|
||||
React.useEffect(() => {
|
||||
if (!repostChannel && channelStrings) {
|
||||
const channels = channelStrings.split(',');
|
||||
const newChannelUrl = channels[0];
|
||||
const { claimName } = parseURI(newChannelUrl);
|
||||
setRepostChannel(claimName);
|
||||
}
|
||||
}, [channelStrings]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (enteredRepostName && isNameValid(enteredRepostName, false)) {
|
||||
doCheckPublishNameAvailability(enteredRepostName).then(r => setAvailable(r));
|
||||
}
|
||||
}, [enteredRepostName, doCheckPublishNameAvailability]);
|
||||
|
||||
// takeover amount, bid suggestion
|
||||
React.useEffect(() => {
|
||||
const repostTakeoverAmount = Number(enteredRepostAmount)
|
||||
? Number(enteredRepostAmount) + 0.01
|
||||
: Number(passedRepostAmount) + 0.01;
|
||||
|
||||
if (repostTakeoverAmount) {
|
||||
setTakeoverAmount(Number(repostTakeoverAmount.toFixed(2)));
|
||||
setAutoRepostBid(repostTakeoverAmount);
|
||||
}
|
||||
}, [setTakeoverAmount, enteredRepostAmount, passedRepostAmount]);
|
||||
|
||||
// repost bid error
|
||||
React.useEffect(() => {
|
||||
let rBidError;
|
||||
if (repostBid === 0) {
|
||||
rBidError = __('Deposit cannot be 0');
|
||||
} else if (balance === repostBid) {
|
||||
rBidError = __('Please decrease your deposit to account for transaction fees');
|
||||
} else if (balance < repostBid) {
|
||||
rBidError = __('Deposit cannot be higher than your balance');
|
||||
} else if (repostBid < MINIMUM_PUBLISH_BID) {
|
||||
rBidError = __('Your deposit must be higher');
|
||||
}
|
||||
setRepostBidError(rBidError);
|
||||
}, [setRepostBidError, repostBidError, repostBid]);
|
||||
|
||||
// setContentUri given enteredUri
|
||||
React.useEffect(() => {
|
||||
if (!enteredContent && !contentFirstRender.current) {
|
||||
setContentError(__('A name is required'));
|
||||
}
|
||||
if (enteredContent) {
|
||||
contentFirstRender.current = false;
|
||||
const [searchContent, error] = getSearchUri(enteredContent);
|
||||
if (error) {
|
||||
setContentError(__('Something not quite right..'));
|
||||
} else {
|
||||
setContentError('');
|
||||
}
|
||||
try {
|
||||
const { streamName, channelName, isChannel } = parseURI(searchContent);
|
||||
if (!isChannel && streamName && isNameValid(streamName)) {
|
||||
// contentNameValid = true;
|
||||
setContentUri(searchContent);
|
||||
} else if (isChannel && channelName && isNameValid(channelName)) {
|
||||
// contentNameValid = true;
|
||||
setContentUri(searchContent);
|
||||
}
|
||||
} catch (e) {
|
||||
if (enteredContent !== '@') setContentError('');
|
||||
setContentUri(``);
|
||||
}
|
||||
}
|
||||
}, [enteredContent, setContentUri, setContentError, parseURI, isNameValid]);
|
||||
|
||||
// setRepostName
|
||||
React.useEffect(() => {
|
||||
if (enteredRepostName) {
|
||||
let isValid = false;
|
||||
try {
|
||||
parseURI(enteredRepostName);
|
||||
isValid = true;
|
||||
} catch (e) {
|
||||
isValid = false;
|
||||
}
|
||||
if (isValid) {
|
||||
setRepostUri(enteredRepostName);
|
||||
}
|
||||
}
|
||||
}, [enteredRepostName, setRepostUri, parseURI]);
|
||||
|
||||
const repostClaimId = contentClaimId || enteredClaimId;
|
||||
|
||||
const getRedirect = (entered, passed, redirect) => {
|
||||
if (redirect) {
|
||||
return redirect;
|
||||
} else if (entered) {
|
||||
try {
|
||||
const { claimName } = parseURI(entered);
|
||||
return `/${claimName}`;
|
||||
} catch (e) {
|
||||
return '/';
|
||||
}
|
||||
} else if (passed) {
|
||||
try {
|
||||
const { claimName } = parseURI(passed);
|
||||
return `/${claimName}`;
|
||||
} catch (e) {
|
||||
return '/';
|
||||
}
|
||||
} else {
|
||||
return '/';
|
||||
}
|
||||
};
|
||||
|
||||
function handleSubmit() {
|
||||
const channelToRepostTo = channels && channels.find(channel => channel.name === repostChannel);
|
||||
if (enteredRepostName && repostBid && repostClaimId) {
|
||||
doRepost({
|
||||
name: enteredRepostName,
|
||||
bid: creditsToString(repostBid),
|
||||
channel_id: channelToRepostTo ? channelToRepostTo.claim_id : undefined,
|
||||
claim_id: repostClaimId,
|
||||
}).then((repostClaim: StreamClaim) => {
|
||||
doCheckPendingClaims();
|
||||
analytics.apiLogPublish(repostClaim);
|
||||
doToast({ message: __('Woohoo! Successfully reposted this claim.') });
|
||||
replace(getRedirect(contentUri, uri, redirectUri));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function cancelIt() {
|
||||
doClearRepostError();
|
||||
goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
actions={
|
||||
<div>
|
||||
{uri && (
|
||||
<fieldset-section>
|
||||
<ClaimPreview key={uri} uri={uri} actions={''} type={'inline'} showNullPlaceholder />
|
||||
</fieldset-section>
|
||||
)}
|
||||
{!uri && name && (
|
||||
<>
|
||||
<FormField
|
||||
label={'Content to repost'}
|
||||
type="text"
|
||||
name="content_url"
|
||||
value={enteredContent}
|
||||
error={contentError}
|
||||
onChange={event => setEnteredContentUri(event.target.value)}
|
||||
placeholder={__('Enter a name or %domain% URL', { domain: SITE_URL })}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!uri && (
|
||||
<fieldset-section>
|
||||
<ClaimPreview key={contentUri} uri={contentUri} actions={''} type={'large'} showNullPlaceholder />
|
||||
</fieldset-section>
|
||||
)}
|
||||
|
||||
<React.Fragment>
|
||||
<fieldset-section>
|
||||
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
|
||||
<fieldset-section>
|
||||
<label htmlFor="auth_first_channel">
|
||||
{repostNameError ? (
|
||||
<span className="error__text">{repostNameError}</span>
|
||||
) : (
|
||||
<span>
|
||||
{__('Repost URL')}
|
||||
<HelpLink href="https://lbry.com/faq/naming" />
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<div className="form-field__prefix">{repostUrlName}</div>
|
||||
</fieldset-section>
|
||||
<FormField
|
||||
type="text"
|
||||
name="repost_name"
|
||||
value={enteredRepostName}
|
||||
onChange={event => setEnteredRepostName(event.target.value)}
|
||||
placeholder={__('Do a thing')}
|
||||
/>
|
||||
</fieldset-group>
|
||||
</fieldset-section>
|
||||
<SelectChannel
|
||||
label={__('Channel to repost on')}
|
||||
hideNew
|
||||
channel={repostChannel}
|
||||
onChannelChange={newChannel => setRepostChannel(newChannel)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
type="number"
|
||||
name="repost_bid"
|
||||
min="0"
|
||||
step="any"
|
||||
placeholder="0.123"
|
||||
className="form-field--price-amount"
|
||||
label={<LbcSymbol postfix={__('Support')} size={14} />}
|
||||
value={repostBid}
|
||||
error={repostBidError}
|
||||
helper={__('Winning amount: %amount%', {
|
||||
amount: Number(takeoverAmount).toFixed(2),
|
||||
})}
|
||||
disabled={!enteredRepostName || resolvingRepost}
|
||||
onChange={event => setRepostBid(event.target.value)}
|
||||
onWheel={e => e.stopPropagation()}
|
||||
/>
|
||||
</React.Fragment>
|
||||
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
icon={ICONS.REPOST}
|
||||
disabled={
|
||||
resolvingRepost ||
|
||||
reposting ||
|
||||
repostBidError ||
|
||||
repostNameError ||
|
||||
((!uri || enteredContent) && contentNameError) ||
|
||||
(!uri && !enteredContentClaim)
|
||||
}
|
||||
button="primary"
|
||||
label={reposting ? __('Reposting') : __('Repost')}
|
||||
onClick={handleSubmit}
|
||||
/>
|
||||
<Button button="link" label={__('Cancel')} onClick={cancelIt} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default RepostCreate;
|
|
@ -43,6 +43,7 @@ import CreatorDashboard from 'page/creatorDashboard';
|
|||
import RewardsVerifyPage from 'page/rewardsVerify';
|
||||
import CheckoutPage from 'page/checkoutPage';
|
||||
import ChannelNew from 'page/channelNew';
|
||||
import RepostNew from 'page/repost';
|
||||
import BuyPage from 'page/buy';
|
||||
import NotificationsPage from 'page/notifications';
|
||||
import SignInWalletPasswordPage from 'page/signInWalletPassword';
|
||||
|
@ -262,6 +263,7 @@ function AppRouter(props: Props) {
|
|||
/>
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.CHANNEL_NEW}`} component={ChannelNew} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.REPOST_NEW}`} component={RepostNew} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.UPLOADS}`} component={FileListPublished} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.CREATOR_DASHBOARD}`} component={CreatorDashboard} />
|
||||
<PrivateRoute {...props} path={`/$/${PAGES.UPLOAD}`} component={PublishPage} />
|
||||
|
|
|
@ -1,12 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doResolveUris } from 'lbry-redux';
|
||||
import { doResolveUris, doClearPublish, doPrepareEdit, selectPendingIds } from 'lbry-redux';
|
||||
import { makeSelectWinningUriForQuery } from 'redux/selectors/search';
|
||||
import SearchTopClaim from './view';
|
||||
import { push } from 'connected-react-router';
|
||||
import * as PAGES from 'constants/pages';
|
||||
|
||||
const select = (state, props) => ({
|
||||
winningUri: makeSelectWinningUriForQuery(props.query)(state),
|
||||
pendingIds: selectPendingIds(state),
|
||||
});
|
||||
|
||||
export default connect(select, {
|
||||
doResolveUris,
|
||||
})(SearchTopClaim);
|
||||
const perform = dispatch => ({
|
||||
beginPublish: name => {
|
||||
dispatch(doClearPublish());
|
||||
dispatch(doPrepareEdit({ name }));
|
||||
dispatch(push(`/$/${PAGES.UPLOAD}`));
|
||||
},
|
||||
doResolveUris: uris => dispatch(doResolveUris(uris)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SearchTopClaim);
|
||||
|
|
|
@ -6,8 +6,11 @@ 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';
|
||||
import { useHistory } from 'react-router';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import { DOMAIN } from 'config';
|
||||
import Yrbl from 'component/yrbl';
|
||||
|
||||
type Props = {
|
||||
query: string,
|
||||
|
@ -15,20 +18,26 @@ type Props = {
|
|||
doResolveUris: (Array<string>) => void,
|
||||
hideLink?: boolean,
|
||||
setChannelActive: boolean => void,
|
||||
beginPublish: string => void,
|
||||
pendingIds: Array<string>,
|
||||
};
|
||||
|
||||
export default function SearchTopClaim(props: Props) {
|
||||
const { doResolveUris, query = '', winningUri, hideLink = false, setChannelActive } = props;
|
||||
const { doResolveUris, query = '', winningUri, hideLink = false, setChannelActive, beginPublish } = props;
|
||||
const uriFromQuery = `lbry://${query}`;
|
||||
|
||||
const { push } = useHistory();
|
||||
let name;
|
||||
let channelUriFromQuery;
|
||||
let winningUriIsChannel;
|
||||
try {
|
||||
const { isChannel } = parseURI(uriFromQuery);
|
||||
const { isChannel, streamName, channelName } = parseURI(uriFromQuery);
|
||||
const { isChannel: winnerIsChannel } = parseURI(winningUri);
|
||||
winningUriIsChannel = winnerIsChannel;
|
||||
if (!isChannel) {
|
||||
channelUriFromQuery = `lbry://@${query}`;
|
||||
name = streamName;
|
||||
} else {
|
||||
name = channelName;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
|
@ -51,19 +60,27 @@ export default function SearchTopClaim(props: Props) {
|
|||
}
|
||||
}, [doResolveUris, uriFromQuery, channelUriFromQuery]);
|
||||
|
||||
if (!winningUri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="search">
|
||||
<header className="search__header">
|
||||
{winningUri && (
|
||||
<div className="claim-preview__actions--header">
|
||||
<span className="media__uri">
|
||||
{__('Most supported')}
|
||||
<HelpLink href="https://lbry.com/faq/tipping" />
|
||||
</span>
|
||||
<a
|
||||
className="media__uri"
|
||||
href="https://lbry.com/faq/trending"
|
||||
title={__('Learn more about LBRY Credits on %DOMAIN%', { DOMAIN })}
|
||||
>
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
lbc: <LbcSymbol />,
|
||||
}}
|
||||
>
|
||||
Most supported %lbc%
|
||||
</I18nMessage>
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{winningUri && (
|
||||
<div className="card">
|
||||
<ClaimPreview
|
||||
hideRepostLabel
|
||||
|
@ -77,7 +94,34 @@ export default function SearchTopClaim(props: Props) {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
{!hideLink && (
|
||||
)}
|
||||
{!winningUri && uriFromQuery && (
|
||||
<div className="empty empty--centered">
|
||||
<Yrbl
|
||||
type="happy"
|
||||
title={__('Whoa!')}
|
||||
small
|
||||
subtitle={
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
repost: (
|
||||
<Button
|
||||
button="link"
|
||||
onClick={() => push(`/$/${PAGES.REPOST_NEW}?to=${name}`)}
|
||||
label={__('Repost')}
|
||||
/>
|
||||
),
|
||||
publish: <Button button="link" onClick={() => beginPublish(name)} label={'publish'} />,
|
||||
name: <strong>name</strong>,
|
||||
}}
|
||||
>
|
||||
You have found the edge of the internet. %repost% or %publish% your stuff here to claim this spot.
|
||||
</I18nMessage>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!hideLink && winningUri && (
|
||||
<div className="section__actions--between section__actions--no-margin">
|
||||
<span />
|
||||
<Button
|
||||
|
|
|
@ -11,6 +11,7 @@ type Props = {
|
|||
className?: string,
|
||||
actions?: Node,
|
||||
alwaysShow?: boolean,
|
||||
small: boolean,
|
||||
};
|
||||
|
||||
const yrblTypes = {
|
||||
|
@ -24,7 +25,7 @@ export default class extends React.PureComponent<Props> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { title, subtitle, type, className, actions, alwaysShow = false } = this.props;
|
||||
const { title, subtitle, type, className, actions, small, alwaysShow = false } = this.props;
|
||||
|
||||
const image = yrblTypes[type];
|
||||
|
||||
|
@ -32,7 +33,7 @@ export default class extends React.PureComponent<Props> {
|
|||
<div className="yrbl__wrap">
|
||||
<img
|
||||
alt="Friendly gerbil"
|
||||
className={classnames('yrbl', className, {
|
||||
className={classnames(small ? 'yrbl--small' : 'yrbl', className, {
|
||||
'yrbl--always-show': alwaysShow,
|
||||
})}
|
||||
src={`${image}`}
|
||||
|
|
|
@ -19,6 +19,7 @@ exports.GET_CREDITS = 'getcredits';
|
|||
exports.REPORT = 'report';
|
||||
exports.REWARDS = 'rewards';
|
||||
exports.REWARDS_VERIFY = 'rewards/verify';
|
||||
exports.REPOST_NEW = 'repost';
|
||||
exports.SEND = 'send';
|
||||
exports.SETTINGS = 'settings';
|
||||
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
|
||||
|
|
|
@ -12,7 +12,7 @@ import { parseURI, isNameValid, creditsToString } from 'lbry-redux';
|
|||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import analytics from 'analytics';
|
||||
import LbcSymbol from '../../component/common/lbc-symbol';
|
||||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
|
||||
type Props = {
|
||||
doHideModal: () => void,
|
||||
|
|
|
@ -6,7 +6,6 @@ import YrblWalletEmpty from 'component/yrblWalletEmpty';
|
|||
|
||||
type Props = {
|
||||
balance: number,
|
||||
totalRewardValue: number,
|
||||
};
|
||||
|
||||
function PublishPage(props: Props) {
|
||||
|
|
15
ui/page/repost/index.js
Normal file
15
ui/page/repost/index.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import { doResolveUri, selectBalance } from 'lbry-redux';
|
||||
|
||||
import RepostPage from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
balance: selectBalance(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(RepostPage);
|
77
ui/page/repost/view.jsx
Normal file
77
ui/page/repost/view.jsx
Normal file
|
@ -0,0 +1,77 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import Page from 'component/page';
|
||||
import { useHistory } from 'react-router';
|
||||
import RepostCreate from 'component/repostCreate';
|
||||
import YrblWalletEmpty from 'component/yrblWalletEmpty';
|
||||
import useThrottle from 'effects/use-throttle';
|
||||
import classnames from 'classnames';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
resolveUri: string => void,
|
||||
};
|
||||
function RepostPage(props: Props) {
|
||||
const { balance, resolveUri } = props;
|
||||
|
||||
const REPOST_FROM = 'from';
|
||||
const REPOST_TO = 'to';
|
||||
const REDIRECT = 'redirect';
|
||||
const {
|
||||
location: { search },
|
||||
} = useHistory();
|
||||
|
||||
const urlParams = new URLSearchParams(search);
|
||||
const repostFrom = urlParams.get(REPOST_FROM);
|
||||
const redirectUri = urlParams.get(REDIRECT);
|
||||
const repostTo = urlParams.get(REPOST_TO);
|
||||
const [contentUri, setContentUri] = React.useState('');
|
||||
const [repostUri, setRepostUri] = React.useState('');
|
||||
const throttledContentValue = useThrottle(contentUri, 500);
|
||||
const throttledRepostValue = useThrottle(repostUri, 500);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (throttledContentValue) {
|
||||
resolveUri(throttledContentValue);
|
||||
}
|
||||
}, [throttledContentValue, resolveUri]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (throttledRepostValue) {
|
||||
resolveUri(throttledRepostValue);
|
||||
}
|
||||
}, [throttledRepostValue, resolveUri]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (repostTo) {
|
||||
resolveUri(repostTo);
|
||||
}
|
||||
}, [repostTo, resolveUri]);
|
||||
|
||||
const decodedFrom = repostFrom && decodeURIComponent(repostFrom);
|
||||
return (
|
||||
<Page
|
||||
noFooter
|
||||
noSideNavigation
|
||||
backout={{
|
||||
title: __('Repost'),
|
||||
backLabel: __('Back'),
|
||||
}}
|
||||
>
|
||||
{balance === 0 && <YrblWalletEmpty />}
|
||||
<div className={classnames({ 'card--disabled': balance === 0 })}>
|
||||
<RepostCreate
|
||||
uri={decodedFrom}
|
||||
name={repostTo}
|
||||
redirectUri={redirectUri}
|
||||
repostUri={repostUri}
|
||||
contentUri={contentUri}
|
||||
setContentUri={setContentUri}
|
||||
setRepostUri={setRepostUri}
|
||||
/>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export default RepostPage;
|
|
@ -11,6 +11,7 @@ import Ads from 'web/component/ads';
|
|||
import SearchTopClaim from 'component/searchTopClaim';
|
||||
import { formatLbryUrlForWeb } from 'util/url';
|
||||
import { useHistory } from 'react-router';
|
||||
import ClaimPreview from 'component/claimPreview';
|
||||
|
||||
type AdditionalOptions = {
|
||||
isBackgroundSearch: boolean,
|
||||
|
@ -97,7 +98,6 @@ export default function SearchPage(props: Props) {
|
|||
{urlQuery && (
|
||||
<>
|
||||
{isValid && <SearchTopClaim query={modifiedUrlQuery} />}
|
||||
|
||||
<ClaimList
|
||||
uris={uris}
|
||||
loading={isSearching}
|
||||
|
@ -121,6 +121,7 @@ export default function SearchPage(props: Props) {
|
|||
</>
|
||||
}
|
||||
/>
|
||||
{isSearching && new Array(5).fill(1).map((x, i) => <ClaimPreview key={i} placeholder="loading" />)}
|
||||
|
||||
<div className="main--empty help">{__('These search results are provided by LBRY, Inc.')}</div>
|
||||
</>
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { connect } from 'react-redux';
|
||||
import TopPage from './view';
|
||||
import { doClearPublish, doPrepareEdit, doResolveUris } from 'lbry-redux';
|
||||
import { push } from 'connected-react-router';
|
||||
import * as PAGES from 'constants/pages';
|
||||
|
||||
const select = (state, props) => {
|
||||
const { search } = props.location;
|
||||
|
@ -11,4 +14,13 @@ const select = (state, props) => {
|
|||
};
|
||||
};
|
||||
|
||||
export default connect(select)(TopPage);
|
||||
const perform = dispatch => ({
|
||||
beginPublish: name => {
|
||||
dispatch(doClearPublish());
|
||||
dispatch(doPrepareEdit({ name }));
|
||||
dispatch(push(`/$/${PAGES.UPLOAD}`));
|
||||
},
|
||||
doResolveUris: uris => dispatch(doResolveUris(uris)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(TopPage);
|
||||
|
|
|
@ -7,22 +7,42 @@ 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';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import * as PAGES from 'constants/pages';
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
beginPublish: string => void,
|
||||
};
|
||||
|
||||
function TopPage(props: Props) {
|
||||
const { name } = props;
|
||||
const { name, beginPublish } = props;
|
||||
const [channelActive, setChannelActive] = React.useState(false);
|
||||
|
||||
// if the query was actually '@name', still offer repost for 'name'
|
||||
const queryName = name[0] === '@' ? name.slice(1) : name;
|
||||
return (
|
||||
<Page>
|
||||
<SearchTopClaim query={name} hideLink setChannelActive={setChannelActive} />
|
||||
<ClaimListDiscover
|
||||
name={channelActive ? `@${name}` : name}
|
||||
name={channelActive ? `@${queryName}` : queryName}
|
||||
defaultFreshness={FRESH_ALL}
|
||||
defaultOrderBy={ORDER_BY_TOP}
|
||||
meta={
|
||||
<I18nMessage
|
||||
tokens={{
|
||||
repost: (
|
||||
<Button
|
||||
button="secondary"
|
||||
navigate={`/$/${PAGES.REPOST_NEW}?to=${queryName}`}
|
||||
label={__('Repost Here')}
|
||||
/>
|
||||
),
|
||||
publish: <Button button="secondary" onClick={() => beginPublish(queryName)} label={'Publish Here'} />,
|
||||
}}
|
||||
>
|
||||
%repost% %publish%
|
||||
</I18nMessage>
|
||||
}
|
||||
includeSupportAction
|
||||
renderProperties={claim => (
|
||||
<span className="claim-preview__custom-properties">
|
||||
|
@ -33,7 +53,7 @@ function TopPage(props: Props) {
|
|||
header={
|
||||
<div className="claim-search__menu-group">
|
||||
<Button
|
||||
label={name}
|
||||
label={queryName}
|
||||
button="alt"
|
||||
onClick={() => setChannelActive(false)}
|
||||
className={classnames('button-toggle', {
|
||||
|
@ -41,7 +61,7 @@ function TopPage(props: Props) {
|
|||
})}
|
||||
/>
|
||||
<Button
|
||||
label={`@${name}`}
|
||||
label={`@${queryName}`}
|
||||
button="alt"
|
||||
onClick={() => setChannelActive(true)}
|
||||
className={classnames('button-toggle', {
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
// @flow
|
||||
import { getSearchQueryString } from 'util/query-params';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { parseURI, makeSelectClaimForUri, makeSelectClaimIsNsfw, buildURI, SETTINGS, isClaimNsfw } from 'lbry-redux';
|
||||
import {
|
||||
parseURI,
|
||||
makeSelectClaimForUri,
|
||||
makeSelectClaimIsNsfw,
|
||||
buildURI,
|
||||
SETTINGS,
|
||||
isClaimNsfw,
|
||||
makeSelectPendingClaimForUri,
|
||||
} from 'lbry-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
type State = { search: SearchState };
|
||||
|
@ -79,7 +87,6 @@ export const makeSelectRecommendedContentForUri = (uri: string) =>
|
|||
recommendedContent = searchUris;
|
||||
}
|
||||
}
|
||||
|
||||
return recommendedContent;
|
||||
}
|
||||
);
|
||||
|
@ -97,11 +104,13 @@ export const makeSelectWinningUriForQuery = (query: string) => {
|
|||
|
||||
return createSelector(
|
||||
makeSelectClientSetting(SETTINGS.SHOW_MATURE),
|
||||
makeSelectPendingClaimForUri(uriFromQuery),
|
||||
makeSelectClaimForUri(uriFromQuery),
|
||||
makeSelectClaimForUri(channelUriFromQuery),
|
||||
(matureEnabled, claim1, claim2) => {
|
||||
(matureEnabled, pendingClaim, claim1, claim2) => {
|
||||
const claim1Mature = claim1 && isClaimNsfw(claim1);
|
||||
const claim2Mature = claim2 && isClaimNsfw(claim2);
|
||||
let pendingAmount = pendingClaim && pendingClaim.amount;
|
||||
|
||||
if (!claim1 && !claim2) {
|
||||
return undefined;
|
||||
|
@ -124,7 +133,13 @@ export const makeSelectWinningUriForQuery = (query: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
return Number(effectiveAmount1) > Number(effectiveAmount2) ? claim1.canonical_url : claim2.canonical_url;
|
||||
const returnBeforePending =
|
||||
Number(effectiveAmount1) > Number(effectiveAmount2) ? claim1.canonical_url : claim2.canonical_url;
|
||||
if (pendingAmount && pendingAmount > effectiveAmount1 && pendingAmount > effectiveAmount2) {
|
||||
return pendingAmount.permanent_url;
|
||||
} else {
|
||||
return returnBeforePending;
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -58,3 +58,4 @@
|
|||
@import 'component/tags';
|
||||
@import 'component/wunderbar';
|
||||
@import 'component/yrbl';
|
||||
@import 'component/empty';
|
||||
|
|
|
@ -98,6 +98,12 @@
|
|||
margin-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.media__thumb-placeholder-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-m);
|
||||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
@include handleChannelGif(6rem);
|
||||
}
|
||||
|
@ -115,29 +121,47 @@
|
|||
font-size: 14px;
|
||||
|
||||
.channel-thumbnail {
|
||||
@include handleChannelGif(5rem);
|
||||
@include handleChannelGif(4rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.claim-preview__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.claim-preview--large {
|
||||
border: none;
|
||||
min-height: 8rem;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
.media__thumb {
|
||||
width: 14rem;
|
||||
@include handleClaimListGifThumbnail(14rem);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
width: 5rem;
|
||||
min-height: 5rem;
|
||||
width: 8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-thumbnail {
|
||||
width: 7.5rem;
|
||||
@include handleChannelGif(7.5rem);
|
||||
|
||||
@media (max-width: $breakpoint-small) {
|
||||
width: 5rem;
|
||||
@include handleChannelGif(5rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -201,6 +225,9 @@
|
|||
|
||||
.claim-preview__custom-properties {
|
||||
text-align: right;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.claim-preview-metadata {
|
||||
|
@ -523,3 +550,7 @@
|
|||
.claim-link {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.claim-preview__null-label {
|
||||
margin: auto;
|
||||
}
|
||||
|
|
|
@ -97,3 +97,15 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
.claim-search__menu-group--between {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
&:last-of-type {
|
||||
.button-toggle:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
21
ui/scss/component/_empty.scss
Normal file
21
ui/scss/component/_empty.scss
Normal file
|
@ -0,0 +1,21 @@
|
|||
.empty__wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
text-align: left;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
.empty__text {
|
||||
color: var(--color-text-empty);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.empty__content {
|
||||
max-width: 400px;
|
||||
}
|
|
@ -34,6 +34,16 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.yrbl--small {
|
||||
display: none;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
display: block;
|
||||
height: 8rem;
|
||||
margin-right: calc(var(--spacing-xl));
|
||||
}
|
||||
}
|
||||
|
||||
.yrbl__content {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
|
|
@ -64,7 +64,8 @@
|
|||
--color-text: #d8d8d8;
|
||||
--color-text-error: #f5748c;
|
||||
--color-text-help-warning: #f5ec74;
|
||||
--color-text-empty: #bbbbbb;
|
||||
--color-text-empty: var(--color-text-subtitle);
|
||||
|
||||
--color-text-help: #bbbbbb;
|
||||
--color-text-subtitle: #9fafc0;
|
||||
--color-text-warning: #212529;
|
||||
|
@ -74,7 +75,7 @@
|
|||
|
||||
// Input
|
||||
--color-input: #f4f4f5;
|
||||
--color-input-label: #d4d4d4;
|
||||
--color-input-label: #a7a7a7;
|
||||
--color-input-placeholder: #f4f4f5;
|
||||
--color-input-bg: #464d53;
|
||||
--color-input-bg-copyable: #4f5861;
|
||||
|
|
|
@ -7498,9 +7498,9 @@ lazy-val@^1.0.4:
|
|||
yargs "^13.2.2"
|
||||
zstd-codec "^0.1.1"
|
||||
|
||||
lbry-redux@lbryio/lbry-redux#92a4263c908f6df5727c379a3b3348935891d806:
|
||||
lbry-redux@lbryio/lbry-redux#664df6237d777d7cce31e58479bc98563ea98cb9:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/92a4263c908f6df5727c379a3b3348935891d806"
|
||||
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/664df6237d777d7cce31e58479bc98563ea98cb9"
|
||||
dependencies:
|
||||
proxy-polyfill "0.1.6"
|
||||
reselect "^3.0.0"
|
||||
|
|
Loading…
Reference in a new issue