From a87e15754a308c9e742cc3661e88f71c098aeea0 Mon Sep 17 00:00:00 2001 From: Oleg Silkin Date: Sat, 30 Nov 2019 14:39:51 -0500 Subject: [PATCH] Implements the `ChannelForm` and `ChannelCreate` components --- ui/component/channelCreate/index.js | 24 ++++ ui/component/channelCreate/view.jsx | 192 ++++++++++++++++++++++++++++ ui/component/channelForm/index.js | 40 ++++++ ui/component/channelForm/view.jsx | 72 +++++++++++ 4 files changed, 328 insertions(+) create mode 100644 ui/component/channelCreate/index.js create mode 100644 ui/component/channelCreate/view.jsx create mode 100644 ui/component/channelForm/index.js create mode 100644 ui/component/channelForm/view.jsx diff --git a/ui/component/channelCreate/index.js b/ui/component/channelCreate/index.js new file mode 100644 index 000000000..4a4476f93 --- /dev/null +++ b/ui/component/channelCreate/index.js @@ -0,0 +1,24 @@ +import { connect } from 'react-redux'; +import ChannelCreate from './view'; +import { + selectBalance, + selectMyChannelClaims, + selectFetchingMyChannels, + doFetchChannelListMine, + doCreateChannel, +} from 'lbry-redux'; +import { selectUserVerifiedEmail } from 'lbryinc'; + +const select = state => ({ + channels: selectMyChannelClaims(state), + fetchingChannels: selectFetchingMyChannels(state), + balance: selectBalance(state), + emailVerified: selectUserVerifiedEmail(state), +}); + +const perform = dispatch => ({ + createChannel: (name, amount) => dispatch(doCreateChannel(name, amount)), + fetchChannelListMine: () => dispatch(doFetchChannelListMine()), +}); + +export default connect(select, perform)(ChannelCreate); diff --git a/ui/component/channelCreate/view.jsx b/ui/component/channelCreate/view.jsx new file mode 100644 index 000000000..2ea11a5cf --- /dev/null +++ b/ui/component/channelCreate/view.jsx @@ -0,0 +1,192 @@ +// @flow +import React, { Fragment } from 'react'; +import { isNameValid } from 'lbry-redux'; +import { FormField } from 'component/common/form'; +import BusyIndicator from 'component/common/busy-indicator'; +import Button from 'component/button'; +import analytics from 'analytics'; + +import { CHANNEL_NEW, MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR } from 'constants/claim'; + +type Props = { + channel: string, // currently selected channel + channels: ?Array, + balance: number, + onChannelChange: string => void, + createChannel: (string, number) => Promise, + fetchChannelListMine: () => void, + fetchingChannels: boolean, + emailVerified: boolean, + onSuccess: () => void, +}; + +type State = { + newChannelName: string, + newChannelBid: number, + addingChannel: boolean, + creatingChannel: boolean, + newChannelNameError: string, + newChannelBidError: string, + createChannelError: ?string, +}; + +class ChannelCreate extends React.PureComponent { + constructor(props: Props) { + super(props); + + this.state = { + newChannelName: '', + newChannelBid: 0.1, + addingChannel: false, + creatingChannel: false, + newChannelNameError: '', + newChannelBidError: '', + createChannelError: undefined, + }; + + (this: any).handleChannelChange = this.handleChannelChange.bind(this); + (this: any).handleNewChannelNameChange = this.handleNewChannelNameChange.bind(this); + (this: any).handleNewChannelBidChange = this.handleNewChannelBidChange.bind(this); + (this: any).handleCreateChannelClick = this.handleCreateChannelClick.bind(this); + } + + handleChannelChange(event: SyntheticInputEvent<*>) { + const { onChannelChange } = this.props; + const { newChannelBid } = this.state; + const channel = event.target.value; + + if (channel === CHANNEL_NEW) { + this.setState({ addingChannel: true }); + onChannelChange(channel); + this.handleNewChannelBidChange(newChannelBid); + } else { + this.setState({ addingChannel: false }); + onChannelChange(channel); + } + } + + handleNewChannelNameChange(event: SyntheticInputEvent<*>) { + let newChannelName = event.target.value; + + if (newChannelName.startsWith('@')) { + newChannelName = newChannelName.slice(1); + } + + let newChannelNameError; + if (newChannelName.length > 0 && !isNameValid(newChannelName, false)) { + newChannelNameError = INVALID_NAME_ERROR; + } + + this.setState({ + newChannelNameError, + newChannelName, + }); + } + + handleNewChannelBidChange(newChannelBid: number) { + const { balance } = this.props; + let newChannelBidError; + if (newChannelBid === 0) { + newChannelBidError = __('Your deposit cannot be 0'); + } else if (newChannelBid === balance) { + newChannelBidError = __('Please decrease your deposit to account for transaction fees'); + } else if (newChannelBid > balance) { + newChannelBidError = __('Deposit cannot be higher than your balance'); + } else if (newChannelBid < MINIMUM_PUBLISH_BID) { + newChannelBidError = __('Your deposit must be higher'); + } + + this.setState({ + newChannelBid, + newChannelBidError, + }); + } + + handleCreateChannelClick() { + const { balance, createChannel, onChannelChange } = this.props; + const { newChannelBid, newChannelName } = this.state; + + const channelName = `@${newChannelName.trim()}`; + + if (newChannelBid > balance) { + return; + } + + this.setState({ + creatingChannel: true, + createChannelError: undefined, + }); + + const success = channelClaim => { + this.setState({ + creatingChannel: false, + addingChannel: false, + }); + analytics.apiLogPublish(channelClaim); + onChannelChange(channelName); + this.props.onSuccess(); + }; + + const failure = () => { + this.setState({ + creatingChannel: false, + createChannelError: __('Unable to create channel due to an internal error.'), + }); + }; + + createChannel(channelName, newChannelBid).then(success, failure); + } + + render() { + const { + newChannelName, + newChannelNameError, + newChannelBid, + newChannelBidError, + creatingChannel, + createChannelError, + } = this.state; + + return ( + + {createChannelError &&
{createChannelError}
} +
+ + this.handleNewChannelBidChange(parseFloat(event.target.value))} + /> +
+
+
+ {creatingChannel && } +
+ ); + } +} + +export default ChannelCreate; diff --git a/ui/component/channelForm/index.js b/ui/component/channelForm/index.js new file mode 100644 index 000000000..dd0be32f2 --- /dev/null +++ b/ui/component/channelForm/index.js @@ -0,0 +1,40 @@ +import { connect } from 'react-redux'; +import { + doResolveUri, + selectPublishFormValues, + selectIsStillEditing, + selectMyClaimForUri, + selectIsResolvingPublishUris, + selectTakeOverAmount, + doResetThumbnailStatus, + doClearPublish, + doUpdatePublishForm, + doPrepareEdit, +} from 'lbry-redux'; +import { doPublishDesktop } from 'redux/actions/publish'; +import { selectUnclaimedRewardValue } from 'lbryinc'; +import ChannelForm from './view'; + +const select = state => ({ + ...selectPublishFormValues(state), + // The winning claim for a short lbry uri + amountNeededForTakeover: selectTakeOverAmount(state), + // My previously published claims under this short lbry uri + myClaimForUri: selectMyClaimForUri(state), + // If I clicked the "edit" button, have I changed the uri? + // Need this to make it easier to find the source on previously published content + isStillEditing: selectIsStillEditing(state), + isResolvingUri: selectIsResolvingPublishUris(state), + totalRewardValue: selectUnclaimedRewardValue(state), +}); + +const perform = dispatch => ({ + updatePublishForm: value => dispatch(doUpdatePublishForm(value)), + clearPublish: () => dispatch(doClearPublish()), + resolveUri: uri => dispatch(doResolveUri(uri)), + publish: filePath => dispatch(doPublishDesktop(filePath)), + prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)), + resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()), +}); + +export default connect(select, perform)(ChannelForm); diff --git a/ui/component/channelForm/view.jsx b/ui/component/channelForm/view.jsx new file mode 100644 index 000000000..97c1e400d --- /dev/null +++ b/ui/component/channelForm/view.jsx @@ -0,0 +1,72 @@ +// @flow + +/* + On submit, this component calls publish, which dispatches doPublishDesktop. + doPublishDesktop calls lbry-redux Lbry publish method using lbry-redux publish state as params. + Publish simply instructs the SDK to find the file path on disk and publish it with the provided metadata. + On web, the Lbry publish method call is overridden in platform/web/api-setup, using a function in platform/web/publish. + File upload is carried out in the background by that function. + */ +import React, { useEffect, Fragment } from 'react'; +import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim'; +import { buildURI, isURIValid } from 'lbry-redux'; +import ChannelCreate from 'component/channelCreate'; +import Card from 'component/common/card'; +import * as ICONS from 'constants/icons'; + +type Props = { + name: ?string, + channel: string, + resolveUri: string => void, + // Add back type + updatePublishForm: any => void, + onSuccess: () => void, +}; + +function ChannelForm(props: Props) { + const { name, channel, resolveUri, updatePublishForm, onSuccess } = props; + + // Every time the channel or name changes, resolve the uris to find winning bid amounts + useEffect(() => { + // If they are midway through a channel creation, treat it as anonymous until it completes + const channelName = channel === CHANNEL_ANONYMOUS || channel === CHANNEL_NEW ? '' : channel; + + // We are only going to store the full uri, but we need to resolve the uri with and without the channel name + let uri; + try { + uri = name && buildURI({ streamName: name, channelName }); + } catch (e) {} + + if (channelName && name) { + // resolve without the channel name so we know the winning bid for it + try { + const uriLessChannel = buildURI({ streamName: name }); + resolveUri(uriLessChannel); + } catch (e) {} + } + + const isValid = isURIValid(uri); + if (uri && isValid) { + resolveUri(uri); + updatePublishForm({ uri }); + } + }, [name, channel, resolveUri, updatePublishForm]); + + return ( + + + updatePublishForm({ channel })} /> + + } + /> + + ); +} + +export default ChannelForm;