// @flow import { DOMAIN } from 'config'; import React from 'react'; import classnames from 'classnames'; import Button from 'component/button'; import TagsSearch from 'component/tagsSearch'; import ErrorText from 'component/common/error-text'; import ClaimAbandonButton from 'component/claimAbandonButton'; import ChannelSelector from 'component/channelSelector'; import ClaimList from 'component/claimList'; import Card from 'component/common/card'; import LbcSymbol from 'component/common/lbc-symbol'; import ThumbnailPicker from 'component/thumbnailPicker'; import { useHistory } from 'react-router-dom'; import { isNameValid, regexInvalidURI } from 'lbry-redux'; import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; import { FormField } from 'component/common/form'; import { handleBidChange } from 'util/publish'; import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field'; import { INVALID_NAME_ERROR } from 'constants/claim'; import SUPPORTED_LANGUAGES from 'constants/supported_languages'; import * as PAGES from 'constants/pages'; import analytics from 'analytics'; const LANG_NONE = 'none'; const MAX_TAG_SELECT = 5; type Props = { uri: string, claim: CollectionClaim, balance: number, disabled: boolean, activeChannelClaim: ?ChannelClaim, incognito: boolean, // params title: string, amount: number, thumbnailUrl: string, description: string, tags: Array<string>, locations: Array<string>, languages: Array<string>, collectionId: string, collection: Collection, collectionClaimIds: Array<string>, collectionUrls: Array<string>, publishCollectionUpdate: (CollectionUpdateParams) => Promise<any>, updatingCollection: boolean, updateError: string, publishCollection: (CollectionPublishParams, string) => Promise<any>, createError: string, creatingCollection: boolean, clearCollectionErrors: () => void, onDone: (string) => void, openModal: ( id: string, { onUpdate: (string) => void, assetName: string, helpText: string, currentValue: string, title: string } ) => void, }; function CollectionForm(props: Props) { const { uri, // collection uri claim, balance, // publish params amount, title, description, thumbnailUrl, tags, locations, languages = [], // rest onDone, publishCollectionUpdate, updateError, updatingCollection, publishCollection, creatingCollection, createError, disabled, activeChannelClaim, incognito, collectionId, collection, collectionUrls, collectionClaimIds, clearCollectionErrors, } = props; const activeChannelName = activeChannelClaim && activeChannelClaim.name; let prefix = IS_WEB ? `${DOMAIN}/` : 'lbry://'; if (activeChannelName && !incognito) { prefix += `${activeChannelName}/`; } const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id; const collectionName = (claim && claim.name) || (collection && collection.name); const [nameError, setNameError] = React.useState(undefined); const [bidError, setBidError] = React.useState(''); const [params, setParams]: [any, (any) => void] = React.useState(getCollectionParams()); const name = params.name; const isNewCollection = !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]; const collectionClaimIdsString = JSON.stringify(collectionClaimIds); function parseName(newName) { let INVALID_URI_CHARS = new RegExp(regexInvalidURI, 'gu'); return newName.replace(INVALID_URI_CHARS, '-'); } function setParam(paramObj) { setParams({ ...params, ...paramObj }); } function getCollectionParams() { const collectionParams: { thumbnail_url?: string, name?: string, description?: string, title?: string, bid: string, languages?: ?Array<string>, locations?: ?Array<string>, tags?: ?Array<{ name: string }>, claim_id?: string, channel_id?: string, claims: ?Array<string>, } = { thumbnail_url: thumbnailUrl, description, bid: String(amount || 0.001), languages: languages || [], locations: locations || [], tags: tags ? tags.map((tag) => { return { name: tag }; }) : [], claims: collectionClaimIds, }; collectionParams['name'] = parseName(collectionName); if (activeChannelId) { collectionParams['channel_id'] = activeChannelId; } if (!claim) { collectionParams['title'] = collectionName; } if (claim) { collectionParams['claim_id'] = claim.claim_id; collectionParams['title'] = title; } return collectionParams; } React.useEffect(() => { const collectionClaimIds = JSON.parse(collectionClaimIdsString); setParams({ ...params, claims: collectionClaimIds }); clearCollectionErrors(); }, [collectionClaimIdsString, setParams]); 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, thumbnail_url: thumbnailUrl }); } function handleSubmit() { if (uri) { publishCollectionUpdate(params).then((pendingClaim) => { if (pendingClaim) { const claimId = pendingClaim.claim_id; analytics.apiLogPublish(pendingClaim); onDone(claimId); } }); } else { publishCollection(params, collectionId).then((pendingClaim) => { if (pendingClaim) { const claimId = pendingClaim.claim_id; analytics.apiLogPublish(pendingClaim); onDone(claimId); } }); } } 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(() => { if (incognito) { const newParams = Object.assign({}, params); delete newParams.channel_id; setParams(newParams); } else if (activeChannelId) { setParams({ ...params, channel_id: activeChannelId }); } }, [activeChannelId, incognito, setParams]); const itemError = !params.claims.length ? __('Cannot publish empty list') : ''; const submitError = nameError || bidError || itemError || updateError || createError; return ( <> <div className={classnames('main--contained', { 'card--disabled': disabled })}> <Tabs> <TabList className="tabs__list--collection-edit-page"> <Tab>{__('General')}</Tab> <Tab>{__('Items')}</Tab> <Tab>{__('Credits')}</Tab> <Tab>{__('Tags')}</Tab> <Tab>{__('Other')}</Tab> </TabList> <TabPanels> <TabPanel> <div className={'card-stack'}> <ChannelSelector disabled={disabled} /> <Card body={ <> <fieldset-group className="fieldset-group--smushed fieldset-group--disabled-prefix"> <fieldset-section> <label htmlFor="channel_name">{__('Channel')}</label> </fieldset-section> </fieldset-group> <fieldset-group class="fieldset-group--smushed fieldset-group--disabled-prefix"> <fieldset-section> <label htmlFor="channel_name">{__('Name')}</label> <div className="form-field__prefix">{prefix}</div> </fieldset-section> <FormField autoFocus={isNewCollection} type="text" name="channel_name" placeholder={__('MyAwesomeList')} value={params.name} error={nameError} disabled={!isNewCollection} onChange={(e) => setParams({ ...params, name: e.target.value || '' })} /> </fieldset-group> {!isNewCollection && ( <span className="form-field__help">{__('This field cannot be changed.')}</span> )} <FormField type="text" name="channel_title2" label={__('Title')} placeholder={__('My Awesome List')} value={params.title} onChange={(e) => setParams({ ...params, title: e.target.value })} /> <ThumbnailPicker inline thumbnailParam={params.thumbnail_url} updateThumbnailParam={handleThumbnailChange} /> <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} /> </> } /> </div> </TabPanel> <TabPanel> <ClaimList uris={collectionUrls} collectionId={collectionId} empty={__('This list has no items.')} /> </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.bid} error={bidError} min="0.0" disabled={false} onChange={(event) => handleBidChange(parseFloat(event.target.value), amount, balance, setBidError, setParam) } placeholder={0.1} helper={__('Increasing your deposit can help your channel be discovered more easily.')} /> } /> </TabPanel> <TabPanel> <Card body={ <TagsSearch suggestMature disableAutoFocus 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 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={creatingCollection || updatingCollection || nameError || bidError || !params.claims.length} label={creatingCollection || updatingCollection ? __('Submitting') : __('Submit')} onClick={handleSubmit} /> <Button button="link" label={__('Cancel')} onClick={() => onDone(collectionId)} /> </div> {submitError ? ( <ErrorText>{submitError}</ErrorText> ) : ( <p className="help"> {__('After submitting, it will take a few minutes for your changes to be live for everyone.')} </p> )} {!isNewCollection && ( <div className="section__actions"> <ClaimAbandonButton uri={uri} abandonActionCallback={() => replace(`/$/${PAGES.LIBRARY}`)} /> </div> )} </> } /> </div> </> ); } export default CollectionForm;