82aaf361f1
## Changes: 1) TagSearch: hide the "control tags" in the Creator Settings page (irrelevant). 2) TagSearch: show the "control tags" when creating/editing Channel (let's use `setting.CommentsEnabled` instead). 3) TagSearch: show the "control tags" when creating/editing Content (`disable-comments` can be used to block comments at the per-claim level, e.g. allow comments in general but block only for specific claims). ## Missing pieces: For (2) and (3), some work is needed to hide the comment GUI when `setting.CommentsEnabled` is disabled for a particular channel. That flag is not ready in Commentron yet, so I'm not sure how this will be done at the moment. In other words, the checkbox does nothing at the moment. ## Potential flaw: This change will hide all control tags. If we have more tags in the future and would like to selectively disable some, we'll have to change this parameter to an array instead. Since the usage is not widespread at the moment, a single `disableControlFlag` seems cleaner (don't over-think it yet).
487 lines
17 KiB
JavaScript
487 lines
17 KiB
JavaScript
// @flow
|
|
import * as MODALS from 'constants/modal_types';
|
|
import * as ICONS from 'constants/icons';
|
|
import React from 'react';
|
|
import classnames from 'classnames';
|
|
import { FormField } from 'component/common/form';
|
|
import Button from 'component/button';
|
|
import TagsSearch from 'component/tagsSearch';
|
|
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
|
|
import ErrorText from 'component/common/error-text';
|
|
import ChannelThumbnail from 'component/channelThumbnail';
|
|
import { isNameValid, parseURI } from 'lbry-redux';
|
|
import ClaimAbandonButton from 'component/claimAbandonButton';
|
|
import { useHistory } from 'react-router-dom';
|
|
import { MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR, ESTIMATED_FEE } from 'constants/claim';
|
|
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs';
|
|
import Card from 'component/common/card';
|
|
import * as PAGES from 'constants/pages';
|
|
import analytics from 'analytics';
|
|
import LbcSymbol from 'component/common/lbc-symbol';
|
|
import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
|
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
|
import { SIMPLE_SITE } from 'config';
|
|
|
|
const LANG_NONE = 'none';
|
|
|
|
const MAX_TAG_SELECT = 5;
|
|
|
|
type Props = {
|
|
claim: ChannelClaim,
|
|
title: string,
|
|
amount: number,
|
|
coverUrl: string,
|
|
thumbnailUrl: string,
|
|
location: { search: string },
|
|
description: string,
|
|
website: string,
|
|
email: string,
|
|
balance: number,
|
|
tags: Array<string>,
|
|
locations: Array<string>,
|
|
languages: Array<string>,
|
|
updateChannel: (any) => Promise<any>,
|
|
updatingChannel: boolean,
|
|
updateError: string,
|
|
createChannel: (any) => Promise<any>,
|
|
createError: string,
|
|
creatingChannel: boolean,
|
|
clearChannelErrors: () => void,
|
|
onDone: () => void,
|
|
openModal: (
|
|
id: string,
|
|
{ onUpdate: (string) => void, assetName: string, helpText: string, currentValue: string, title: string }
|
|
) => void,
|
|
uri: string,
|
|
disabled: boolean,
|
|
};
|
|
|
|
function ChannelForm(props: Props) {
|
|
const {
|
|
uri,
|
|
claim,
|
|
amount,
|
|
title,
|
|
description,
|
|
website,
|
|
email,
|
|
thumbnailUrl,
|
|
coverUrl,
|
|
tags,
|
|
locations,
|
|
languages = [],
|
|
onDone,
|
|
updateChannel,
|
|
updateError,
|
|
updatingChannel,
|
|
createChannel,
|
|
creatingChannel,
|
|
createError,
|
|
clearChannelErrors,
|
|
openModal,
|
|
disabled,
|
|
} = props;
|
|
const [nameError, setNameError] = React.useState(undefined);
|
|
const [bidError, setBidError] = React.useState('');
|
|
const [coverError, setCoverError] = React.useState(false);
|
|
const { claim_id: claimId } = claim || {};
|
|
const [params, setParams]: [any, (any) => void] = React.useState(getChannelParams());
|
|
const { channelName } = parseURI(uri);
|
|
const name = params.name;
|
|
const isNewChannel = !uri;
|
|
const { replace } = useHistory();
|
|
const languageParam = params.languages;
|
|
const primaryLanguage = Array.isArray(languageParam) && languageParam.length && languageParam[0];
|
|
const secondaryLanguage = Array.isArray(languageParam) && languageParam.length >= 2 && languageParam[1];
|
|
|
|
function getChannelParams() {
|
|
// fill this in with sdk data
|
|
const channelParams: {
|
|
website: string,
|
|
email: string,
|
|
coverUrl: string,
|
|
thumbnailUrl: string,
|
|
description: string,
|
|
title: string,
|
|
amount: number,
|
|
languages: ?Array<string>,
|
|
locations: ?Array<string>,
|
|
tags: ?Array<{ name: string }>,
|
|
claim_id?: string,
|
|
} = {
|
|
website,
|
|
email,
|
|
coverUrl,
|
|
thumbnailUrl,
|
|
description,
|
|
title,
|
|
amount: amount || 0.001,
|
|
languages: languages || [],
|
|
locations: locations || [],
|
|
tags: tags
|
|
? tags.map((tag) => {
|
|
return { name: tag };
|
|
})
|
|
: [],
|
|
};
|
|
|
|
if (claimId) {
|
|
channelParams['claim_id'] = claimId;
|
|
}
|
|
|
|
return channelParams;
|
|
}
|
|
|
|
function handleBidChange(bid: number) {
|
|
const { balance, amount } = props;
|
|
const totalAvailableBidAmount = (parseFloat(amount) || 0.0) + (parseFloat(balance) || 0.0);
|
|
|
|
setParams({ ...params, amount: bid });
|
|
|
|
if (bid <= 0.0 || isNaN(bid)) {
|
|
setBidError(__('Deposit cannot be 0'));
|
|
} else if (totalAvailableBidAmount < bid) {
|
|
setBidError(
|
|
__('Deposit cannot be higher than your available balance: %balance%', { balance: totalAvailableBidAmount })
|
|
);
|
|
} else if (totalAvailableBidAmount - bid < ESTIMATED_FEE) {
|
|
setBidError(__('Please decrease your deposit to account for transaction fees'));
|
|
} else if (bid < MINIMUM_PUBLISH_BID) {
|
|
setBidError(__('Your deposit must be higher'));
|
|
} else {
|
|
setBidError('');
|
|
}
|
|
}
|
|
|
|
function handleLanguageChange(index, code) {
|
|
let langs = [...languageParam];
|
|
if (index === 0) {
|
|
if (code === LANG_NONE) {
|
|
// clear all
|
|
langs = [];
|
|
} else {
|
|
langs[0] = code;
|
|
}
|
|
} else {
|
|
if (code === LANG_NONE || code === langs[0]) {
|
|
langs.splice(1, 1);
|
|
} else {
|
|
langs[index] = code;
|
|
}
|
|
}
|
|
setParams({ ...params, languages: langs });
|
|
}
|
|
|
|
function handleThumbnailChange(thumbnailUrl: string) {
|
|
setParams({ ...params, thumbnailUrl });
|
|
}
|
|
|
|
function handleCoverChange(coverUrl: string) {
|
|
setParams({ ...params, coverUrl });
|
|
}
|
|
|
|
function handleSubmit() {
|
|
if (uri) {
|
|
updateChannel(params).then((success) => {
|
|
if (success) {
|
|
onDone();
|
|
}
|
|
});
|
|
} else {
|
|
createChannel(params).then((success) => {
|
|
if (success) {
|
|
analytics.apiLogPublish(success);
|
|
onDone();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const LIMIT_ERR_PARTIAL_MSG = 'bad-txns-claimscriptsize-toolarge (code 16)';
|
|
let errorMsg = updateError || createError;
|
|
if (errorMsg && errorMsg.includes(LIMIT_ERR_PARTIAL_MSG)) {
|
|
errorMsg = __('Transaction limit reached. Try reducing the Description length.');
|
|
}
|
|
|
|
React.useEffect(() => {
|
|
let nameError;
|
|
if (!name && name !== undefined) {
|
|
nameError = __('A name is required for your url');
|
|
} else if (!isNameValid(name, false)) {
|
|
nameError = INVALID_NAME_ERROR;
|
|
}
|
|
|
|
setNameError(nameError);
|
|
}, [name]);
|
|
|
|
React.useEffect(() => {
|
|
clearChannelErrors();
|
|
}, [clearChannelErrors]);
|
|
|
|
// TODO clear and bail after submit
|
|
return (
|
|
<>
|
|
<div className={classnames('main--contained', { 'card--disabled': disabled })}>
|
|
<header className="channel-cover">
|
|
<div className="channel__quick-actions">
|
|
<Button
|
|
button="alt"
|
|
title={__('Cover')}
|
|
onClick={() =>
|
|
openModal(MODALS.IMAGE_UPLOAD, {
|
|
onUpdate: (coverUrl) => handleCoverChange(coverUrl),
|
|
title: __('Edit Cover Image'),
|
|
helpText: __('(6.25:1)'),
|
|
assetName: __('Cover Image'),
|
|
currentValue: params.coverUrl,
|
|
})
|
|
}
|
|
icon={ICONS.CAMERA}
|
|
iconSize={18}
|
|
/>
|
|
</div>
|
|
{params.coverUrl &&
|
|
(coverError ? (
|
|
<div className="channel-cover__custom--waiting">{__('This will be visible in a few minutes.')}</div>
|
|
) : (
|
|
<img className="channel-cover__custom" src={params.coverUrl} onError={() => setCoverError(true)} />
|
|
))}
|
|
<div className="channel__primary-info">
|
|
<div className="channel__edit-thumb">
|
|
<Button
|
|
button="alt"
|
|
title={__('Edit')}
|
|
onClick={() =>
|
|
openModal(MODALS.IMAGE_UPLOAD, {
|
|
onUpdate: (v) => handleThumbnailChange(v),
|
|
title: __('Edit Thumbnail Image'),
|
|
helpText: __('(1:1)'),
|
|
assetName: __('Thumbnail'),
|
|
currentValue: params.thumbnailUrl,
|
|
})
|
|
}
|
|
icon={ICONS.CAMERA}
|
|
iconSize={18}
|
|
/>
|
|
</div>
|
|
<ChannelThumbnail
|
|
className="channel__thumbnail--channel-page"
|
|
uri={uri}
|
|
thumbnailPreview={params.thumbnailUrl}
|
|
allowGifs
|
|
showDelayedMessage
|
|
/>
|
|
<h1 className="channel__title">
|
|
{params.title || (channelName && '@' + channelName) || (params.name && '@' + params.name)}
|
|
</h1>
|
|
</div>
|
|
<div className="channel-cover__gradient" />
|
|
</header>
|
|
|
|
<Tabs>
|
|
<TabList className="tabs__list--channel-page">
|
|
<Tab>{__('General')}</Tab>
|
|
<Tab>{__('Credit Details')}</Tab>
|
|
<Tab>{__('Tags')}</Tab>
|
|
<Tab>{__('Other')}</Tab>
|
|
</TabList>
|
|
<TabPanels>
|
|
<TabPanel>
|
|
<Card
|
|
body={
|
|
<>
|
|
<fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix">
|
|
<fieldset-section>
|
|
<label htmlFor="channel_name">{__('Name')}</label>
|
|
<div className="form-field__prefix">@</div>
|
|
</fieldset-section>
|
|
|
|
<FormField
|
|
autoFocus={isNewChannel}
|
|
type="text"
|
|
name="channel_name"
|
|
placeholder={__('MyAwesomeChannel')}
|
|
value={params.name || channelName}
|
|
error={nameError}
|
|
disabled={!isNewChannel}
|
|
onChange={(e) => setParams({ ...params, name: e.target.value })}
|
|
/>
|
|
</fieldset-group>
|
|
{!isNewChannel && <span className="form-field__help">{__('This field cannot be changed.')}</span>}
|
|
|
|
<FormField
|
|
type="text"
|
|
name="channel_title2"
|
|
label={__('Title')}
|
|
placeholder={__('My Awesome Channel')}
|
|
value={params.title}
|
|
onChange={(e) => setParams({ ...params, title: e.target.value })}
|
|
/>
|
|
<FormField
|
|
type="markdown"
|
|
name="content_description2"
|
|
label={__('Description')}
|
|
placeholder={__('Description of your content')}
|
|
value={params.description}
|
|
onChange={(text) => setParams({ ...params, description: text })}
|
|
textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION}
|
|
/>
|
|
</>
|
|
}
|
|
/>
|
|
</TabPanel>
|
|
<TabPanel>
|
|
<Card
|
|
body={
|
|
<FormField
|
|
className="form-field--price-amount"
|
|
type="number"
|
|
name="content_bid2"
|
|
step="any"
|
|
label={<LbcSymbol postfix={__('Deposit')} size={14} />}
|
|
value={params.amount}
|
|
error={bidError}
|
|
min="0.0"
|
|
disabled={false}
|
|
onChange={(event) => handleBidChange(parseFloat(event.target.value))}
|
|
placeholder={0.1}
|
|
helper={
|
|
<>
|
|
{__('Increasing your deposit can help your channel be discovered more easily.')}
|
|
<WalletSpendableBalanceHelp inline />
|
|
</>
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
</TabPanel>
|
|
<TabPanel>
|
|
<Card
|
|
body={
|
|
<TagsSearch
|
|
suggestMature={!SIMPLE_SITE}
|
|
disableAutoFocus
|
|
disableControlTags
|
|
limitSelect={MAX_TAG_SELECT}
|
|
tagsPassedIn={params.tags || []}
|
|
label={__('Selected Tags')}
|
|
onRemove={(clickedTag) => {
|
|
const newTags = params.tags.slice().filter((tag) => tag.name !== clickedTag.name);
|
|
setParams({ ...params, tags: newTags });
|
|
}}
|
|
onSelect={(newTags) => {
|
|
newTags.forEach((newTag) => {
|
|
if (!params.tags.map((savedTag) => savedTag.name).includes(newTag.name)) {
|
|
setParams({ ...params, tags: [...params.tags, newTag] });
|
|
} else {
|
|
// If it already exists and the user types it in, remove it
|
|
setParams({ ...params, tags: params.tags.filter((tag) => tag.name !== newTag.name) });
|
|
}
|
|
});
|
|
}}
|
|
/>
|
|
}
|
|
/>
|
|
</TabPanel>
|
|
<TabPanel>
|
|
<Card
|
|
body={
|
|
<>
|
|
<FormField
|
|
type="text"
|
|
name="channel_website2"
|
|
label={__('Website')}
|
|
placeholder={__('aprettygoodsite.com')}
|
|
disabled={false}
|
|
value={params.website}
|
|
onChange={(e) => setParams({ ...params, website: e.target.value })}
|
|
/>
|
|
<FormField
|
|
type="text"
|
|
name="content_email2"
|
|
label={__('Email')}
|
|
placeholder={__('yourstruly@example.com')}
|
|
disabled={false}
|
|
value={params.email}
|
|
onChange={(e) => setParams({ ...params, email: e.target.value })}
|
|
/>
|
|
<FormField
|
|
name="language_select"
|
|
type="select"
|
|
label={__('Primary Language')}
|
|
onChange={(event) => handleLanguageChange(0, event.target.value)}
|
|
value={primaryLanguage}
|
|
helper={__('Your main content language')}
|
|
>
|
|
<option key={'pri-langNone'} value={LANG_NONE}>
|
|
{__('None selected')}
|
|
</option>
|
|
{Object.keys(SUPPORTED_LANGUAGES).map((language) => (
|
|
<option key={language} value={language}>
|
|
{SUPPORTED_LANGUAGES[language]}
|
|
</option>
|
|
))}
|
|
</FormField>
|
|
<FormField
|
|
name="language_select2"
|
|
type="select"
|
|
label={__('Secondary Language')}
|
|
onChange={(event) => handleLanguageChange(1, event.target.value)}
|
|
value={secondaryLanguage}
|
|
disabled={!languageParam[0]}
|
|
helper={__('Your other content language')}
|
|
>
|
|
<option key={'sec-langNone'} value={LANG_NONE}>
|
|
{__('None selected')}
|
|
</option>
|
|
{Object.keys(SUPPORTED_LANGUAGES)
|
|
.filter((lang) => lang !== languageParam[0])
|
|
.map((language) => (
|
|
<option key={language} value={language}>
|
|
{SUPPORTED_LANGUAGES[language]}
|
|
</option>
|
|
))}
|
|
</FormField>
|
|
</>
|
|
}
|
|
/>
|
|
</TabPanel>
|
|
</TabPanels>
|
|
</Tabs>
|
|
|
|
<Card
|
|
className="card--after-tabs"
|
|
actions={
|
|
<>
|
|
<div className="section__actions">
|
|
<Button
|
|
button="primary"
|
|
disabled={
|
|
creatingChannel || updatingChannel || nameError || bidError || (isNewChannel && !params.name)
|
|
}
|
|
label={creatingChannel || updatingChannel ? __('Submitting') : __('Submit')}
|
|
onClick={handleSubmit}
|
|
/>
|
|
<Button button="link" label={__('Cancel')} onClick={onDone} />
|
|
</div>
|
|
{errorMsg ? (
|
|
<ErrorText>{errorMsg}</ErrorText>
|
|
) : (
|
|
<p className="help">
|
|
{__('After submitting, it will take a few minutes for your changes to be live for everyone.')}
|
|
</p>
|
|
)}
|
|
{!isNewChannel && (
|
|
<div className="section__actions">
|
|
<ClaimAbandonButton uri={uri} abandonActionCallback={() => replace(`/$/${PAGES.CHANNELS}`)} />
|
|
</div>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default ChannelForm;
|