diff --git a/static/app-strings.json b/static/app-strings.json index 0b4e5c295..dd39a8507 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1259,5 +1259,17 @@ "Opt out of any topics you don't want to receive email about.": "Opt out of any topics you don't want to receive email about.", "Uncheck your email below if you want to stop receiving messages.": "Uncheck your email below if you want to stop receiving messages.", "Remove from Blocked List": "Remove from Blocked List", - "Are you sure you want to remove this from the list?": "Are you sure you want to remove this from the list?" -} \ No newline at end of file + "Are you sure you want to remove this from the list?": "Are you sure you want to remove this from the list?", + "Uncheck your email below if you want to stop receiving messages.": "Uncheck your email below if you want to stop receiving messages.", + "Cover": "Cover", + "Url": "Url", + "New Channel Advanced": "New Channel Advanced", + "WTF": "WTF", + "required": "required", + "A name is required for your url": "A name is required for your url", + "Edit Channel": "Edit Channel", + "Create Channel": "Create Channel", + "This shoul de such a size": "This shoul de such a size", + "Thumbnail This shoul de such a size": "Thumbnail This shoul de such a size", + "Cover This shoul de such a size": "Cover This shoul de such a size" +} diff --git a/ui/component/channelAbout/view.jsx b/ui/component/channelAbout/view.jsx index 98acce7cb..94e16dd80 100644 --- a/ui/component/channelAbout/view.jsx +++ b/ui/component/channelAbout/view.jsx @@ -31,11 +31,13 @@ function ChannelAbout(props: Props) {
- {description && ( -
- -
+ <> + +
+ +
+ )} {email && ( diff --git a/ui/component/channelEdit/index.js b/ui/component/channelEdit/index.js index 224072731..ec91e3aeb 100644 --- a/ui/component/channelEdit/index.js +++ b/ui/component/channelEdit/index.js @@ -3,21 +3,26 @@ import { makeSelectTitleForUri, makeSelectThumbnailForUri, makeSelectCoverForUri, - selectCurrentChannelPage, makeSelectMetadataItemForUri, doUpdateChannel, + doCreateChannel, makeSelectAmountForUri, makeSelectClaimForUri, selectUpdateChannelError, selectUpdatingChannel, + selectCreateChannelError, + selectCreatingChannel, + selectBalance, } from 'lbry-redux'; +import { doOpenModal } from 'redux/actions/app'; + import ChannelPage from './view'; const select = (state, props) => ({ + claim: makeSelectClaimForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state), - thumbnailUrl: makeSelectThumbnailForUri(props.uri)(state), - coverUrl: makeSelectCoverForUri(props.uri)(state), - page: selectCurrentChannelPage(state), + thumbnail: makeSelectThumbnailForUri(props.uri)(state), + cover: makeSelectCoverForUri(props.uri)(state), description: makeSelectMetadataItemForUri(props.uri, 'description')(state), website: makeSelectMetadataItemForUri(props.uri, 'website_url')(state), email: makeSelectMetadataItemForUri(props.uri, 'email')(state), @@ -25,13 +30,20 @@ const select = (state, props) => ({ locations: makeSelectMetadataItemForUri(props.uri, 'locations')(state), languages: makeSelectMetadataItemForUri(props.uri, 'languages')(state), amount: makeSelectAmountForUri(props.uri)(state), - claim: makeSelectClaimForUri(props.uri)(state), updateError: selectUpdateChannelError(state), updatingChannel: selectUpdatingChannel(state), + createError: selectCreateChannelError(state), + creatingChannel: selectCreatingChannel(state), + balance: selectBalance(state), }); const perform = dispatch => ({ + openModal: (modal, props) => dispatch(doOpenModal(modal, props)), updateChannel: params => dispatch(doUpdateChannel(params)), + createChannel: params => { + const { name, amount, ...optionalParams } = params; + return dispatch(doCreateChannel('@' + name, amount, optionalParams)); + }, }); export default connect(select, perform)(ChannelPage); diff --git a/ui/component/channelEdit/view.jsx b/ui/component/channelEdit/view.jsx index 43b0b7ee0..9defac559 100644 --- a/ui/component/channelEdit/view.jsx +++ b/ui/component/channelEdit/view.jsx @@ -1,19 +1,23 @@ // @flow -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import { FormField } from 'component/common/form'; import Button from 'component/button'; -import SelectAsset from 'component/selectAsset'; -import { MINIMUM_PUBLISH_BID } from 'constants/claim'; import TagsSearch from 'component/tagsSearch'; import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field'; import ErrorText from 'component/common/error-text'; +import * as MODALS from 'constants/modal_types'; +import * as ICONS from 'constants/icons'; +import ChannelThumbnail from 'component/channelThumbnail'; +import { isNameValid, parseURI } from 'lbry-redux'; +import ClaimAbandonButton from 'component/claimAbandonButton'; +import { MINIMUM_PUBLISH_BID, INVALID_NAME_ERROR, ESTIMATED_FEE } from 'constants/claim'; type Props = { claim: ChannelClaim, - title: ?string, + title: string, amount: string, - coverUrl: ?string, - thumbnailUrl: ?string, + cover: string, + thumbnail: string, location: { search: string }, description: string, website: string, @@ -23,45 +27,48 @@ type Props = { locations: Array, languages: Array, updateChannel: any => Promise, - updateThumb: string => void, - updateCover: string => void, - doneEditing: () => void, - updateError: string, updatingChannel: boolean, + updateError: string, + createChannel: any => Promise, + createError: string, + creatingChannel: boolean, + onDone: () => void, + openModal: (id: string, { onUpdate: string => void, label: string, helptext: string, currentValue: string }) => void, + uri: string, }; function ChannelForm(props: Props) { const { + uri, claim, title, - coverUrl, description, website, email, - thumbnailUrl, + thumbnail, + cover, tags, locations, languages, - amount, - doneEditing, + onDone, updateChannel, - updateThumb, - updateCover, updateError, updatingChannel, + createChannel, + creatingChannel, + createError, + openModal, } = props; - const { claim_id: claimId } = claim; - + const { claim_id: claimId } = claim || {}; // fill this in with sdk data const channelParams = { website, email, - coverUrl, - thumbnailUrl, + cover, + thumbnail, description, title, - amount, - claim_id: claimId, + amount: 0.001, languages: languages || [], locations: locations || [], tags: tags @@ -71,157 +78,241 @@ function ChannelForm(props: Props) { : [], }; - const [params, setParams] = useState(channelParams); + if (claimId) { + channelParams['claim_id'] = claimId; + } + + const { channelName } = parseURI(uri); + const [params, setParams]: [any, (any) => void] = useState(channelParams); + const [nameError, setNameError] = useState(undefined); const [bidError, setBidError] = useState(''); + const name = params.name; + + 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]); // If a user changes tabs, update the url so it stays on the same page if they refresh. // We don't want to use links here because we can't animate the tab change and using links // would alter the Tab label's role attribute, which should stay role="tab" to work with keyboards/screen readers. const handleBidChange = (bid: number) => { const { balance, amount } = props; - const totalAvailableBidAmount = parseFloat(amount) + parseFloat(balance); + const totalAvailableBidAmount = parseFloat(amount) || 0.0 + parseFloat(balance) || 0.0; setParams({ ...params, amount: bid }); - setBidError(''); + if (bid <= 0.0 || isNaN(bid)) { setBidError(__('Deposit cannot be 0')); - } else if (totalAvailableBidAmount === bid) { + } else if (totalAvailableBidAmount - bid < ESTIMATED_FEE) { setBidError(__('Please decrease your deposit to account for transaction fees')); } else if (totalAvailableBidAmount < bid) { setBidError(__('Deposit cannot be higher than your balance')); } else if (bid < MINIMUM_PUBLISH_BID) { setBidError(__('Your deposit must be higher')); + } else { + setBidError(''); } }; const handleThumbnailChange = (thumbnailUrl: string) => { - setParams({ ...params, thumbnailUrl }); - updateThumb(thumbnailUrl); + setParams({ ...params, thumbnail: thumbnailUrl }); }; const handleCoverChange = (coverUrl: string) => { - setParams({ ...params, coverUrl }); - updateCover(coverUrl); + setParams({ ...params, cover: coverUrl }); }; const handleSubmit = () => { - updateChannel(params).then(success => { - if (success) { - doneEditing(); - } - }); + if (uri) { + updateChannel(params).then(success => { + if (success) { + onDone(); + } + }); + } else { + createChannel(params).then(success => { + if (success) { + onDone(); + } + }); + } }; // TODO clear and bail after submit return ( -
-
- handleThumbnailChange(v)} - currentValue={params.thumbnailUrl} - assetName={'Thumbnail'} - recommended={__('Recommended ratio is 1:1')} - /> - - handleCoverChange(v)} - currentValue={params.coverUrl} - assetName={'Cover'} - recommended={__('Recommended ratio is 6.25:1')} - /> - - setParams({ ...params, title: e.target.value })} - /> - handleBidChange(parseFloat(event.target.value))} - placeholder={0.1} - /> - - setParams({ ...params, website: e.target.value })} - /> - - setParams({ ...params, email: e.target.value })} - /> - - setParams({ ...params, description: text })} - textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION} - /> - - { - 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) }); + <> +
+
+ {uri || `lbry://@${params.name || '...'}`} + {uri && ( +
+ +
+ )} +
+
+ {params.cover && } +
+
+
+ +

+ {params.title || (channelName && '@' + channelName) || (params.name && '@' + params.name)} +

+
+
+
+
+
+ {!uri && ( + setParams({ ...params, name: e.target.value })} + /> + )} + + handleBidChange(parseFloat(event.target.value))} + placeholder={0.1} + /> + setParams({ ...params, title: e.target.value })} + /> + + setParams({ ...params, website: e.target.value })} + /> + + setParams({ ...params, email: e.target.value })} + /> + + setParams({ ...params, description: text })} + textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION} + /> + +
+ { + 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) }); + } + }); + }} + /> +
+
+
+ {updateError || createError ? ( + {updateError || createError} + ) : ( +

+ {__('After submitting, you will not see the changes immediately. Please check back in a few minutes.')} +

+ )} +
- {updateError && updateError.length ? ( - {updateError} - ) : ( -

- {__('After submitting, you will not see the changes immediately. Please check back in a few minutes.')} -

- )} -
-
+
+ ); } diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx index c4b436b98..92a3fa495 100644 --- a/ui/component/common/icon-custom.jsx +++ b/ui/component/common/icon-custom.jsx @@ -662,7 +662,9 @@ export const icons = { ), - [ICONS.OPEN_LOG_FOLDER]: buildIcon(), + [ICONS.OPEN_LOG_FOLDER]: buildIcon( + + ), [ICONS.OPEN_LOG]: buildIcon( @@ -671,4 +673,10 @@ export const icons = { ), + [ICONS.CAMERA]: buildIcon( + + + + + ), }; diff --git a/ui/component/header/view.jsx b/ui/component/header/view.jsx index f350f2efd..e78a668bb 100644 --- a/ui/component/header/view.jsx +++ b/ui/component/header/view.jsx @@ -37,6 +37,7 @@ type Props = { email: ?string, authenticated: boolean, authHeader: boolean, + backout: { backFunction: () => void, backTitle: string }, syncError: ?string, emailToVerify?: string, signOut: () => void, @@ -66,6 +67,7 @@ const Header = (props: Props) => { clearEmailEntry, clearPasswordEntry, emailToVerify, + backout, } = props; // on the verify page don't let anyone escape other than by closing the tab to keep session data consistent @@ -135,201 +137,221 @@ const Header = (props: Props) => { // @endif >
-
-
- - {!authHeader ? ( -
- {(!IS_WEB || authenticated) && ( - -
+ ) : ( + <> +
+
- )} -
- ) : ( - !isVerifyPage && ( -
- {/* Add an empty span here so we can use the same style as above */} - {/* This pushes the close button to the right side */} - - -
- ) + + {!authHeader ? ( +
+ {(!IS_WEB || authenticated) && ( + +
+ )} + + ) : ( + !isVerifyPage && ( +
+ {/* Add an empty span here so we can use the same style as above */} + {/* This pushes the close button to the right side */} + + +
+ ) + )} + {' '} - - - )} - - )} - {assetSource === SOURCE_URL && ( - { - onUpdate(e.target.value); - }} - /> - )} - + accept={accept} + /> + )} + {pathSelected && ( +
+ +
+ + + {uploadStatus} +
+
+ )} + + + + ); } diff --git a/ui/constants/claim.js b/ui/constants/claim.js index 0a0935fd6..b114fd9d3 100644 --- a/ui/constants/claim.js +++ b/ui/constants/claim.js @@ -1,4 +1,5 @@ export const MINIMUM_PUBLISH_BID = 0.0001; +export const ESTIMATED_FEE = 0.048; // .001 + .001 | .048 + .048 = .1 export const CHANNEL_ANONYMOUS = 'anonymous'; export const CHANNEL_NEW = 'new'; diff --git a/ui/constants/icons.js b/ui/constants/icons.js index 96632247d..5cf30aca7 100644 --- a/ui/constants/icons.js +++ b/ui/constants/icons.js @@ -105,5 +105,6 @@ export const PINNED = 'Pinned'; export const BUY = 'Buy'; export const SEND = 'Send'; export const RECEIVE = 'Receive'; +export const CAMERA = 'Camera'; export const OPEN_LOG = 'FilePlus'; export const OPEN_LOG_FOLDER = 'Folder'; diff --git a/ui/constants/modal_types.js b/ui/constants/modal_types.js index 474be314a..ef32f3b30 100644 --- a/ui/constants/modal_types.js +++ b/ui/constants/modal_types.js @@ -42,3 +42,4 @@ export const SIGN_OUT = 'sign_out'; export const LIQUIDATE_SUPPORTS = 'liquidate_supports'; export const CONFIRM_AGE = 'confirm_age'; export const REMOVE_BLOCKED = 'remove_blocked'; +export const IMAGE_UPLOAD = 'image_upload'; diff --git a/ui/constants/pages.js b/ui/constants/pages.js index 3de724663..1ec7c516f 100644 --- a/ui/constants/pages.js +++ b/ui/constants/pages.js @@ -39,3 +39,4 @@ exports.CREATOR_DASHBOARD = 'dashboard'; exports.CHECKOUT = 'checkout'; exports.CODE_2257 = '2257'; exports.BUY = 'buy'; +exports.CHANNEL_NEW = 'channelnew'; diff --git a/ui/modal/modalImageUpload/index.js b/ui/modal/modalImageUpload/index.js new file mode 100644 index 000000000..8a7428e4e --- /dev/null +++ b/ui/modal/modalImageUpload/index.js @@ -0,0 +1,11 @@ +import { connect } from 'react-redux'; +import { doHideModal } from 'redux/actions/app'; +import ModalImageUpload from './view'; + +const perform = dispatch => () => ({ + closeModal: () => { + dispatch(doHideModal()); + }, +}); + +export default connect(null, perform)(ModalImageUpload); diff --git a/ui/modal/modalImageUpload/view.jsx b/ui/modal/modalImageUpload/view.jsx new file mode 100644 index 000000000..abc4972ec --- /dev/null +++ b/ui/modal/modalImageUpload/view.jsx @@ -0,0 +1,41 @@ +// @flow +import React from 'react'; +import { Modal } from 'modal/modal'; +import Button from 'component/button'; +import Card from 'component/common/card'; +import SelectAsset from 'component/selectAsset'; + +type Props = { + closeModal: () => void, + currentValue: string, + label: string, + helptext: string, + onUpdate: string => void, +}; + +const ModalImageUpload = (props: Props) => { + const { closeModal, currentValue, label, helptext, onUpdate } = props; + + return ( + + onUpdate(v)} + currentValue={currentValue} + assetName={label} + recommended={__(helptext)} + /> + } + actions={ +
+
+ } + /> +
+ ); +}; + +export default ModalImageUpload; diff --git a/ui/modal/modalRouter/view.jsx b/ui/modal/modalRouter/view.jsx index cac5a8bc8..d4c7e46ad 100644 --- a/ui/modal/modalRouter/view.jsx +++ b/ui/modal/modalRouter/view.jsx @@ -40,6 +40,7 @@ import ModalSignOut from 'modal/modalSignOut'; import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate'; import ModalConfirmAge from 'modal/modalConfirmAge'; import ModalFileSelection from 'modal/modalFileSelection'; +import ModalImageUpload from 'modal/modalImageUpload'; type Props = { modal: { id: string, modalProps: {} }, @@ -143,6 +144,8 @@ function ModalRouter(props: Props) { return ; case MODALS.REMOVE_BLOCKED: return ; + case MODALS.IMAGE_UPLOAD: + return ; default: return null; } diff --git a/ui/page/channel/index.js b/ui/page/channel/index.js index e352bd1e9..184a51649 100644 --- a/ui/page/channel/index.js +++ b/ui/page/channel/index.js @@ -11,6 +11,7 @@ import { } from 'lbry-redux'; import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; +import { doOpenModal } from 'redux/actions/app'; import ChannelPage from './view'; const select = (state, props) => ({ @@ -28,6 +29,7 @@ const select = (state, props) => ({ }); const perform = dispatch => ({ + openModal: (modal, props) => dispatch(doOpenModal(modal, props)), fetchSubCount: claimId => dispatch(doFetchSubCount(claimId)), }); diff --git a/ui/page/channel/view.jsx b/ui/page/channel/view.jsx index 33d99f37b..847930322 100644 --- a/ui/page/channel/view.jsx +++ b/ui/page/channel/view.jsx @@ -52,14 +52,13 @@ type Props = { function ChannelPage(props: Props) { const { uri, + claim, title, cover, history, location, page, channelIsMine, - thumbnail, - claim, isSubscribed, channelIsBlocked, blackListedOutpoints, @@ -72,11 +71,8 @@ function ChannelPage(props: Props) { const { search } = location; const urlParams = new URLSearchParams(search); const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined; - const [coverError, setCoverError] = useState(false); const { permanent_url: permanentUrl } = claim; const [editing, setEditing] = useState(false); - const [thumbPreview, setThumbPreview] = useState(thumbnail); - const [coverPreview, setCoverPreview] = useState(cover); const [lastYtSyncDate, setLastYtSyncDate] = useState(); const claimId = claim.claim_id; const formattedSubCount = Number(subCount).toLocaleString(); @@ -100,18 +96,14 @@ function ChannelPage(props: Props) { history.push(`${url}${search}`); } - function doneEditing() { + function onDone() { setEditing(false); - setThumbPreview(thumbnail); - setCoverPreview(cover); } useEffect(() => { Lbryio.call('yt', 'get_youtuber', { channel_claim_id: claimId }).then(response => { if (response.is_verified_youtuber) { setLastYtSyncDate(response.last_synced); - } else { - setLastYtSyncDate(undefined); } }); }, [claimId]); @@ -134,6 +126,19 @@ function ChannelPage(props: Props) { } }, [channelIsMine, editing]); + if (editing) { + return ( + + + + ); + } + return ( @@ -153,39 +158,26 @@ function ChannelPage(props: Props) { {!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && } {!isSubscribed && } - {!editing && cover && !coverError && ( + {cover && ( setCoverError(true)} /> )} - {editing && } - {/* component that offers select/upload */}
- {!editing && ( - - )} - {editing && ( - - )} +

{title || '@' + channelName}

{formattedSubCount} {subCount !== 1 ? __('Followers') : __('Follower')} - {channelIsMine && !editing && ( + {channelIsMine && ( <> {pending ? ( {__('Your changes will be live in a few minutes')} @@ -201,15 +193,6 @@ function ChannelPage(props: Props) { )} )} - {channelIsMine && editing && ( -
@@ -220,22 +203,12 @@ function ChannelPage(props: Props) { {editing ? __('Editing Your Channel') : __('About')} {__('Comments')} - - {editing ? ( - setThumbPreview(v)} - updateCover={v => setCoverPreview(v)} - /> - ) : ( - - )} + diff --git a/ui/page/channelNew/index.js b/ui/page/channelNew/index.js new file mode 100644 index 000000000..3e41479df --- /dev/null +++ b/ui/page/channelNew/index.js @@ -0,0 +1,7 @@ +import { connect } from 'react-redux'; +import ChannelNew from './view'; + +const select = () => ({}); +const perform = () => ({}); + +export default connect(select, perform)(ChannelNew); diff --git a/ui/page/channelNew/view.jsx b/ui/page/channelNew/view.jsx new file mode 100644 index 000000000..36c343c38 --- /dev/null +++ b/ui/page/channelNew/view.jsx @@ -0,0 +1,24 @@ +// @flow +import React from 'react'; +import ChannelEdit from 'component/channelEdit'; +import Page from 'component/page'; +import { withRouter } from 'react-router'; + +type Props = { + history: { goBack: () => void }, +}; + +function ChannelNew(props: Props) { + const { history } = props; + return ( + history.goBack(), backTitle: __('Create Channel') }} + className="main--auth-page" + > + + + ); +} + +export default withRouter(ChannelNew); diff --git a/ui/page/channels/view.jsx b/ui/page/channels/view.jsx index b3286adc6..71d9fd79f 100644 --- a/ui/page/channels/view.jsx +++ b/ui/page/channels/view.jsx @@ -8,6 +8,7 @@ import Button from 'component/button'; import YoutubeTransferStatus from 'component/youtubeTransferStatus'; import Spinner from 'component/spinner'; import Card from 'component/common/card'; +import * as PAGES from 'constants/pages'; type Props = { channels: Array, @@ -35,12 +36,14 @@ export default function ChannelsPage(props: Props) { openModal(MODALS.CREATE_CHANNEL)} - /> + <> +