basic repost UI

This commit is contained in:
Sean Yesmunt 2020-02-06 13:49:05 -05:00
parent 799b4843d6
commit 776de49c15
15 changed files with 279 additions and 43 deletions

View file

@ -130,7 +130,7 @@
"imagesloaded": "^4.1.4",
"json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#87ae7faf1c1d5ffa86feb578899596f6ea2a5fd9",
"lbry-redux": "lbryio/lbry-redux#ea215ac31c82338ce60963224380bf4e9ecc1702",
"lbryinc": "lbryio/lbryinc#6a59102c52673502569d2c43bd4ee58c315fb2e4",
"lint-staged": "^7.0.2",
"localforage": "^1.7.1",

View file

@ -930,4 +930,4 @@
"Playing in %seconds_left% seconds": "Playing in %seconds_left% seconds",
"Up Next by %channel%": "Up Next by %channel%",
"Install Now": "Install Now"
}
}

View file

@ -96,6 +96,7 @@ function App(props: Props) {
const urlParams = new URLSearchParams(search);
const rawReferrerParam = urlParams.get('r');
const sanitizedReferrerParam = rawReferrerParam && rawReferrerParam.replace(':', '#');
const wrapperElement = appRef.current;
let uri;
try {
@ -122,7 +123,9 @@ function App(props: Props) {
}, [sanitizedReferrerParam, isRewardApproved, referredRewardAvailable]);
useEffect(() => {
ReactModal.setAppElement(appRef.current);
if (wrapperElement) {
ReactModal.setAppElement(wrapperElement);
}
fetchAccessToken();
// @if TARGET='app'
@ -130,7 +133,7 @@ function App(props: Props) {
fetchTransactions(1, TX_LIST.LATEST_PAGE_SIZE);
fetchChannelListMine(); // This needs to be done for web too...
// @endif
}, [fetchRewards, fetchTransactions, fetchAccessToken, fetchChannelListMine]);
}, [fetchRewards, fetchTransactions, fetchAccessToken, fetchChannelListMine, wrapperElement]);
useEffect(() => {
// $FlowFixMe

View file

@ -0,0 +1,12 @@
// @flow
import React from 'react';
type Props = {
children: any,
};
export default function ErrorText(props: Props) {
const { children } = props;
return <span className="error-text">{children}</span>;
}

View file

@ -78,7 +78,6 @@ export class FormField extends React.PureComponent<Props> {
...inputProps
} = this.props;
const errorMessage = typeof error === 'object' ? error.message : error;
const Wrapper = blockWrap
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
@ -160,7 +159,7 @@ export class FormField extends React.PureComponent<Props> {
input = (
<React.Fragment>
<fieldset-section>
{label && (
{(label || errorMessage) && (
<label htmlFor={name}>
{errorMessage ? <span className="error-text">{errorMessage}</span> : label}
</label>

View file

@ -51,6 +51,12 @@ function FileActions(props: Props) {
label={__('Share')}
onClick={() => openModal(MODALS.SOCIAL_SHARE, { uri, webShareable })}
/>
<Button
button="alt"
icon={ICONS.REPOST}
label={__('Repost')}
onClick={() => openModal(MODALS.REPOST, { uri })}
/>
{!claimIsMine && (
<Button

View file

@ -1,11 +1,9 @@
// @flow
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
import React, { Fragment } from 'react';
import { FormField } from 'component/common/form';
import BusyIndicator from 'component/common/busy-indicator';
import ChannelCreate from 'component/channelCreate';
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
type Props = {
channel: string, // currently selected channel
channels: ?Array<ChannelClaim>,
@ -78,36 +76,30 @@ class ChannelSection extends React.PureComponent<Props, State> {
return (
<Fragment>
{fetchingChannels ? (
<BusyIndicator message="Updating channels" />
) : (
<Fragment>
<FormField
name="channel"
label={label || __('Channel')}
type="select"
onChange={this.handleChannelChange}
value={channel}
>
{!hideAnon && <option value={CHANNEL_ANONYMOUS}>{__('Anonymous')}</option>}
{channels &&
channels.map(({ name, claim_id: claimId }) => (
<option key={claimId} value={name}>
{name}
</option>
))}
{injected &&
injected.map(item => (
<option key={item} value={item}>
{item}
</option>
))}
<option value={CHANNEL_NEW}>{__('New channel...')}</option>}
</FormField>
<FormField
name="channel"
label={label || __('Channel')}
type="select"
onChange={this.handleChannelChange}
value={channel}
>
{!hideAnon && <option value={CHANNEL_ANONYMOUS}>{__('Anonymous')}</option>}
{channels &&
channels.map(({ name, claim_id: claimId }) => (
<option key={claimId} value={name}>
{name}
</option>
))}
{injected &&
injected.map(item => (
<option key={item} value={item}>
{item}
</option>
))}
{!fetchingChannels && <option value={CHANNEL_NEW}>{__('New channel...')}</option>}
</FormField>
{addingChannel && <ChannelCreate onSuccess={this.handleChangeToNewChannel} />}
</Fragment>
)}
{addingChannel && <ChannelCreate onSuccess={this.handleChangeToNewChannel} />}
</Fragment>
);
}

View file

@ -19,6 +19,7 @@ function TransactionListTable(props: Props) {
function isRevokeable(txid: string, nout: number) {
const outpoint = `${txid}:${nout}`;
const { mySupports, myClaims } = props;
return !!mySupports[outpoint] || myClaims.has(outpoint);
}

View file

@ -35,3 +35,4 @@ export const CREATE_CHANNEL = 'create_channel';
export const YOUTUBE_WELCOME = 'youtube_welcome';
export const MOBILE_NAVIGATION = 'mobile_navigation';
export const SET_REFERRER = 'set_referrer';
export const REPOST = 'repost';

View file

@ -0,0 +1,33 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import {
makeSelectClaimForUri,
makeSelectTitleForUri,
selectBalance,
selectMyChannelClaims,
doRepost,
selectRepostError,
selectRepostLoading,
doClearRepostError,
doToast,
} from 'lbry-redux';
import ModalRepost from './view';
const select = (state, props) => ({
channels: selectMyChannelClaims(state),
claim: makeSelectClaimForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state),
balance: selectBalance(state),
error: selectRepostError(state),
reposting: selectRepostLoading(state),
});
export default connect(
select,
{
doHideModal,
doRepost,
doClearRepostError,
doToast,
}
)(ModalRepost);

View file

@ -0,0 +1,187 @@
// @flow
import { CHANNEL_NEW, MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim';
import React from 'react';
import { Modal } from 'modal/modal';
import Card from 'component/common/card';
import Button from 'component/button';
import SelectChannel from 'component/selectChannel';
import ErrorText from 'component/common/error-text';
import { FormField } from 'component/common/form';
import { parseURI, isNameValid, creditsToString } from 'lbry-redux';
type Props = {
doHideModal: () => void,
doToast: ({ message: string }) => void,
doClearRepostError: () => void,
doRepost: StreamRepostOptions => Promise<*>,
title: string,
claim: ?StreamClaim,
balance: number,
channels: ?Array<ChannelClaim>,
error: ?string,
reposting: boolean,
};
function ModalRepost(props: Props) {
const {
doHideModal,
doToast,
doClearRepostError,
doRepost,
title,
claim,
balance,
channels,
error,
reposting,
} = props;
const defaultName = claim && `${claim.name}-repost`;
const originalClaimId = claim && claim.claim_id;
const [repostChannel, setRepostChannel] = React.useState<?{ claimId: string, name: string }>();
const [showAdvanced, setShowAdvanced] = React.useState();
const [repostBid, setRepostBid] = React.useState(0.1);
const [repostName, setRepostName] = React.useState(defaultName);
const [repostNameError, setRepostNameError] = React.useState();
const [repostBidError, setRepostBidError] = React.useState();
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, claimId } = parseURI(newChannelUrl);
setRepostChannel({ name: claimName, claimId });
}
}, [channelStrings]);
React.useEffect(() => {
let bidError;
if (repostBid === 0) {
bidError = __('Deposit cannot be 0');
} else if (balance === repostBid) {
bidError = __('Please decrease your deposit to account for transaction fees');
} else if (balance < repostBid) {
bidError = __('Deposit cannot be higher than your balance');
} else if (repostBid < MINIMUM_PUBLISH_BID) {
bidError = __('Your deposit must be higher');
}
setRepostBidError(bidError);
}, [repostBid, balance]);
React.useEffect(() => {
let nameError;
if (!repostName) {
nameError = __('A name is required');
} else if (!isNameValid(repostName, false)) {
nameError = INVALID_NAME_ERROR;
}
setRepostNameError(nameError);
}, [repostName]);
function handleSubmit() {
if (repostName && repostBid && repostChannel && originalClaimId) {
doRepost({
name: repostName,
bid: creditsToString(repostBid),
channel_id: repostChannel.claimId,
claim_id: originalClaimId,
}).then(() => {
doHideModal();
doToast({ message: __('Woohoo! Sucessfully reposted this claim.') });
});
}
}
function handleCloseModal() {
doClearRepostError();
doHideModal();
}
return (
<Modal isOpen type="card" onAborted={handleCloseModal} onConfirmed={handleCloseModal}>
<Card
title={
<span>
Repost <em>{title}</em>
</span>
}
subtitle={
error && <ErrorText>{__('There was an error reposting this claim. Please try again later.')}</ErrorText>
}
body={
<div>
<SelectChannel
label="Channel to repost on"
hideAnon
channel={repostChannel ? repostChannel.name : undefined}
onChannelChange={newChannel => setRepostChannel(newChannel)}
/>
<div className="section__actions">
{!showAdvanced && (
<Button
button="link"
label={showAdvanced ? 'Hide' : __('Advanced')}
onClick={() => setShowAdvanced(!showAdvanced)}
/>
)}
</div>
{showAdvanced && (
<React.Fragment>
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
<fieldset-section>
<label>{__('Name')}</label>
<div className="form-field__prefix">{`lbry://${
!repostChannel || repostChannel.name === CHANNEL_NEW ? '' : `${repostChannel.name}/`
}`}</div>
</fieldset-section>
<FormField
type="text"
name="repost_name"
value={repostName}
error={repostNameError}
onChange={event => setRepostName(event.target.value)}
/>
</fieldset-group>
<div className="form-field__help">
{__('The name of your repost, something about reposting to help search')}
</div>
<FormField
type="number"
name="repost_bid"
min="0"
step="any"
placeholder="0.123"
className="form-field--price-amount"
label={__('Deposit (LBC)')}
postfix="LBC"
value={repostBid}
error={repostBidError}
disabled={!repostName}
onChange={event => setRepostBid(parseFloat(event.target.value))}
onWheel={e => e.stopPropagation()}
/>
</React.Fragment>
)}
</div>
}
actions={
<React.Fragment>
<Button
disabled={reposting || repostBidError || repostNameError}
button="primary"
label={reposting ? __('Reposting') : __('Repost')}
onClick={handleSubmit}
/>
<Button button="link" label={__('Cancel')} onClick={handleCloseModal} />
</React.Fragment>
}
/>
</Modal>
);
}
export default ModalRepost;

View file

@ -34,6 +34,7 @@ import ModalYoutubeWelcome from 'modal/modalYoutubeWelcome';
import ModalCreateChannel from 'modal/modalChannelCreate';
import ModalMobileNavigation from 'modal/modalMobileNavigation';
import ModalSetReferrer from 'modal/modalSetReferrer';
import ModalRepost from 'modal/modalRepost';
type Props = {
modal: { id: string, modalProps: {} },
@ -125,6 +126,8 @@ function ModalRouter(props: Props) {
return <ModalMobileNavigation {...modalProps} />;
case MODALS.SET_REFERRER:
return <ModalSetReferrer {...modalProps} />;
case MODALS.REPOST:
return <ModalRepost {...modalProps} />;
default:
return null;
}

View file

@ -7,8 +7,6 @@
@extend .button--link;
color: var(--color-text-subtitle);
max-width: 100%;
height: 1.2em;
vertical-align: text-top;
text-align: left;
text-overflow: ellipsis;
transition: color 0.2s;

View file

@ -77,8 +77,9 @@ fieldset-group {
border-bottom-left-radius: var(--border-radius);
border-right: 0;
border-color: var(--color-input-border);
color: var(--color-input-placeholder);
color: var(--color-gray-5);
background-color: var(--color-input-bg);
border-right: 1px solid var(--color-gray-1);
}
}
@ -90,7 +91,7 @@ fieldset-group {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-color: var(--color-input-border);
padding-left: 0;
padding-left: var(--spacing-xsmall);
}
}
}

View file

@ -46,7 +46,7 @@ $breakpoint-medium: 1150px;
--card-box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
// Modal
--modal-width: 440px;
--modal-width: 550px;
// Animation :)
--animation-duration: 0.2s;