diff --git a/package.json b/package.json index a45af625f..e8569647a 100644 --- a/package.json +++ b/package.json @@ -124,7 +124,7 @@ "jsmediatags": "^3.8.1", "json-loader": "^0.5.4", "lbry-format": "https://github.com/lbryio/lbry-format.git", - "lbry-redux": "lbryio/lbry-redux#9a676ee311d573b84d11f402d918aeee77be76e1", + "lbry-redux": "lbryio/lbry-redux#efccab44cb025a14fd81ec05ffca0314710c8529", "lbryinc": "lbryio/lbryinc#43d382d9b74d396a581a74d87e4c53105e04f845", "lint-staged": "^7.0.2", "localforage": "^1.7.1", diff --git a/src/ui/component/channelEdit/index.js b/src/ui/component/channelEdit/index.js new file mode 100644 index 000000000..61742cd00 --- /dev/null +++ b/src/ui/component/channelEdit/index.js @@ -0,0 +1,34 @@ +import { connect } from 'react-redux'; +import { + makeSelectTitleForUri, + makeSelectThumbnailForUri, + makeSelectCoverForUri, + selectCurrentChannelPage, + makeSelectMetadataItemForUri, + doUpdateChannel, + makeSelectAmountForUri, +} from 'lbry-redux'; +import ChannelPage from './view'; + +const select = (state, props) => ({ + title: makeSelectTitleForUri(props.uri)(state), + thumbnail: makeSelectThumbnailForUri(props.uri)(state), + cover: makeSelectCoverForUri(props.uri)(state), + page: selectCurrentChannelPage(state), + description: makeSelectMetadataItemForUri(props.uri, 'description')(state), + website: makeSelectMetadataItemForUri(props.uri, 'website_url')(state), + email: makeSelectMetadataItemForUri(props.uri, 'email')(state), + tags: makeSelectMetadataItemForUri(props.uri, 'tags')(state), + locations: makeSelectMetadataItemForUri(props.uri, 'locations')(state), + languages: makeSelectMetadataItemForUri(props.uri, 'languages')(state), + amount: makeSelectAmountForUri(props.uri)(state), +}); + +const perform = dispatch => ({ + updateChannel: params => dispatch(doUpdateChannel(params)), +}); + +export default connect( + select, + perform +)(ChannelPage); diff --git a/src/ui/component/channelEdit/view.jsx b/src/ui/component/channelEdit/view.jsx new file mode 100644 index 000000000..1ecae9291 --- /dev/null +++ b/src/ui/component/channelEdit/view.jsx @@ -0,0 +1,204 @@ +// @flow +import React, { useState } from 'react'; +import { parseURI } from 'lbry-redux'; +import { Form, FormField } from 'component/common/form'; +import Button from 'component/button'; + +import SelectAsset from '../selectAsset/view'; + +type Props = { + uri: string, + + title: ?string, + amount: string, + cover: ?string, + thumbnail: ?string, + location: { search: string }, + description: string, + website: string, + email: string, + balance: number, + tags: Array, + locations: Array, + languages: Array, + + updateChannel: any => void, + + updateThumb: string => void, + updateCover: string => void, + setEditing: boolean => void, +}; + +function ChannelForm(props: Props) { + const { + uri, + title, + cover, + description, + website, + email, + thumbnail, + tags, + locations, + languages, + amount, + updateChannel, + setEditing, + updateThumb, + updateCover, + } = props; + const { claimId } = parseURI(uri); + + // fill this in with sdk data + const channelParams = { + website: website, + email: email, + languages: languages || [], + cover: cover, + description: description, + locations: locations || [], + title: title, + thumbnail: thumbnail, + tags: tags || [], + claim_id: claimId, + amount: amount, + }; + + const [params, setParams] = useState(channelParams); + const [bidError, setBidError] = useState(''); + + const MINIMUM_PUBLISH_BID = 0.00000001; + // 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); + setParams({ ...params, amount: bid }); + setBidError(''); + if (bid <= 0.0 || isNaN(bid)) { + setBidError(__('Deposit cannot be 0')); + } else if (totalAvailableBidAmount === bid) { + 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')); + } + }; + + const handleThumbnailChange = (url: string) => { + setParams({ ...params, thumbnail: url }); + updateThumb(url); + }; + + const handleCoverChange = (url: string) => { + setParams({ ...params, cover: url }); + updateCover(url); + }; + // TODO clear and bail after submit + return ( +
+
+

{__('We can explain...')}

+

+ {__( + "We know this page won't win any design awards, we just wanted to release a very very very basic version that just barely kinda works so people can use it right now. There is a much nicer version being worked on." + )} +

+
+
updateChannel(channelParams)}> +
+ handleThumbnailChange(v)} + currentValue={params.thumbnail} + assetName={'Thumbnail'} + recommended={'(400x400)'} + /> + + handleCoverChange(v)} + currentValue={params.cover} + assetName={'Cover'} + recommended={'(1000x300)'} + /> + + 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 })} + /> +
+
+
+
+
+ ); +} + +export default ChannelForm; diff --git a/src/ui/component/channelThumbnail/view.jsx b/src/ui/component/channelThumbnail/view.jsx index df3c24142..383be9ec1 100644 --- a/src/ui/component/channelThumbnail/view.jsx +++ b/src/ui/component/channelThumbnail/view.jsx @@ -8,10 +8,11 @@ type Props = { thumbnail: ?string, uri: string, className?: string, + thumbnailPreview: ?string, }; function ChannelThumbnail(props: Props) { - const { thumbnail, uri, className } = props; + const { thumbnail, uri, className, thumbnailPreview } = props; // Generate a random color class based on the first letter of the channel name const { channelName } = parseURI(uri); @@ -24,8 +25,8 @@ function ChannelThumbnail(props: Props) { [colorClassName]: !thumbnail, })} > - {!thumbnail && } - {thumbnail && } + {!thumbnail && } + {thumbnail && } ); } diff --git a/src/ui/component/common/file-selector.jsx b/src/ui/component/common/file-selector.jsx index 817c2412f..63e45338c 100644 --- a/src/ui/component/common/file-selector.jsx +++ b/src/ui/component/common/file-selector.jsx @@ -12,7 +12,7 @@ type FileFilters = { type Props = { type: string, - currentPath: ?string, + currentPath?: ?string, onFileChosen: (string, string) => void, label?: string, placeholder?: string, diff --git a/src/ui/component/selectAsset/index.js b/src/ui/component/selectAsset/index.js new file mode 100644 index 000000000..3aedb8b3e --- /dev/null +++ b/src/ui/component/selectAsset/index.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; +import { doOpenModal } from 'redux/actions/app'; +import SelectThumbnail from './view'; + +const perform = dispatch => ({ + openModal: (modal, props) => dispatch(doOpenModal(modal, props)), +}); + +export default connect( + null, + perform +)(SelectThumbnail); diff --git a/src/ui/component/selectAsset/thumbnail-broken.png b/src/ui/component/selectAsset/thumbnail-broken.png new file mode 100644 index 000000000..f41613f8e Binary files /dev/null and b/src/ui/component/selectAsset/thumbnail-broken.png differ diff --git a/src/ui/component/selectAsset/thumbnail-missing.png b/src/ui/component/selectAsset/thumbnail-missing.png new file mode 100644 index 000000000..af89f2691 Binary files /dev/null and b/src/ui/component/selectAsset/thumbnail-missing.png differ diff --git a/src/ui/component/selectAsset/view.jsx b/src/ui/component/selectAsset/view.jsx new file mode 100644 index 000000000..9596bd9d3 --- /dev/null +++ b/src/ui/component/selectAsset/view.jsx @@ -0,0 +1,142 @@ +// @flow + +import React, { useState } from 'react'; +import { FormField } from 'component/common/form'; +import FileSelector from 'component/common/file-selector'; +import Button from 'component/button'; +import fs from 'fs'; +import path from 'path'; +import uuid from 'uuid/v4'; + +const filters = [ + { + name: __('Thumbnail Image'), + extensions: ['png', 'jpg', 'jpeg', 'gif'], + }, +]; + +const SOURCE_URL = 'url'; +const SOURCE_UPLOAD = 'upload'; +const SPEECH_READY = 'READY'; +const SPEECH_UPLOADING = 'UPLOADING'; +type Props = { + assetName: string, + currentValue: ?string, + onUpdate: string => void, + recommended: string, +}; + +function SelectAsset(props: Props) { + const { onUpdate, assetName, currentValue, recommended } = props; + const [assetSource, setAssetSource] = useState(SOURCE_URL); + const [pathSelected, setPathSelected] = useState(''); + const [uploadStatus, setUploadStatus] = useState(SPEECH_READY); + + function doUploadAsset(filePath, thumbnailBuffer) { + let thumbnail, fileExt, fileName, fileType; + if (filePath) { + thumbnail = fs.readFileSync(filePath); + fileExt = path.extname(filePath); + fileName = path.basename(filePath); + fileType = `image/${fileExt.slice(1)}`; + } else if (thumbnailBuffer) { + thumbnail = thumbnailBuffer; + fileExt = '.png'; + fileName = 'thumbnail.png'; + fileType = 'image/png'; + } else { + return null; + } + + const uploadError = (error = '') => { + console.log('error', error); + }; + + const setUrl = path => { + setUploadStatus(SPEECH_READY); + onUpdate(path); + setAssetSource(SOURCE_URL); + }; + + setUploadStatus(SPEECH_UPLOADING); + + const data = new FormData(); + const name = uuid(); + const file = new File([thumbnail], fileName, { type: fileType }); + data.append('name', name); + data.append('file', file); + + return fetch('https://spee.ch/api/claim/publish', { + method: 'POST', + body: data, + }) + .then(response => response.json()) + .then(json => (json.success ? setUrl(`${json.data.serveUrl}`) : uploadError(json.message))) + .catch(err => uploadError(err.message)); + } + return ( + + + setAssetSource(e.target.value)} + label={__(assetName + ' source')} + > + + + + {assetSource === SOURCE_UPLOAD && ( + <> + {!pathSelected && ( + { + setPathSelected(path); + }} + filters={filters} + /> + )} + {pathSelected && ( +
+ {`...${pathSelected.slice(-18)}`} {uploadStatus}{' '} + {' '} + +
+ )} + + )} + {assetSource === SOURCE_URL && ( + { + onUpdate(e.target.value); + }} + /> + )} +
+
+ ); +} + +export default SelectAsset; diff --git a/src/ui/page/channel/view.jsx b/src/ui/page/channel/view.jsx index db88920ab..afcd2d665 100644 --- a/src/ui/page/channel/view.jsx +++ b/src/ui/page/channel/view.jsx @@ -1,15 +1,18 @@ // @flow -import React from 'react'; +import React, { useState } from 'react'; import { parseURI } from 'lbry-redux'; import Page from 'component/page'; import SubscribeButton from 'component/subscribeButton'; import ShareButton from 'component/shareButton'; import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; import { withRouter } from 'react-router'; +import Button from 'component/button'; import { formatLbryUriForWeb } from 'util/uri'; import ChannelContent from 'component/channelContent'; import ChannelAbout from 'component/channelAbout'; import ChannelThumbnail from 'component/channelThumbnail'; +import ChannelEdit from 'component/channelEdit'; +import * as ICONS from 'constants/icons'; const PAGE_VIEW_QUERY = `view`; const ABOUT_PAGE = `about`; @@ -23,19 +26,24 @@ type Props = { location: { search: string }, history: { push: string => void }, match: { params: { attribute: ?string } }, + channelIsMine: boolean, }; function ChannelPage(props: Props) { - const { uri, title, cover, history, location, page } = props; + const { uri, title, cover, history, location, page, channelIsMine, thumbnail } = props; const { channelName, claimName, claimId } = parseURI(uri); const { search } = location; const urlParams = new URLSearchParams(search); const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined; + const [editing, setEditing] = useState(false); + const [thumbPreview, setThumbPreview] = useState(thumbnail); + const [coverPreview, setCoverPreview] = useState(cover); + // 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 tabIndex = currentView === ABOUT_PAGE ? 1 : 0; + const tabIndex = currentView === ABOUT_PAGE || editing ? 1 : 0; const onTabChange = newTabIndex => { let url = formatLbryUriForWeb(uri); let search = '?'; @@ -52,25 +60,34 @@ function ChannelPage(props: Props) {
- {cover && } - -
- - -
-

{title || channelName}

-

- {claimName} - {claimId && `#${claimId}`} -

-
+ {!editing && cover && } + {editing && } + {/* component that offers select/upload */} +
+ {!editing && } + {editing && ( + + )} +

+ {title || channelName} + {channelIsMine && !editing && ( +

+

+ {claimName} + {claimId && `#${claimId}`} +

- - {__('Content')} - {__('About')} + {__('Content')} + {editing ? __('Editing Your Channel') : __('About')}
@@ -82,7 +99,16 @@ function ChannelPage(props: Props) { - + {editing ? ( + setThumbPreview(v)} + updateCover={v => setCoverPreview(v)} + /> + ) : ( + + )} diff --git a/src/ui/redux/actions/publish.js b/src/ui/redux/actions/publish.js index 63d3263c8..663d26c7d 100644 --- a/src/ui/redux/actions/publish.js +++ b/src/ui/redux/actions/publish.js @@ -345,7 +345,7 @@ export const doPublish = () => (dispatch: Dispatch, getState: () => {}) => { dispatch({ type: ACTIONS.PUBLISH_FAIL }); dispatch(doError(error.message)); }; - + console.log('PP', publishPayload); return Lbry.publish(publishPayload).then(success, failure); }; diff --git a/src/ui/scss/component/_channel.scss b/src/ui/scss/component/_channel.scss index 2d8d7782d..b81abb78e 100644 --- a/src/ui/scss/component/_channel.scss +++ b/src/ui/scss/component/_channel.scss @@ -86,6 +86,12 @@ $metadata-z-index: 1; font-size: 3rem; font-weight: 800; margin-right: var(--spacing-large); + + // Quick hack to get this to work + // We should have a generic style for "header with button next to it" + .button { + margin-left: var(--spacing-medium); + } } .channel__url { diff --git a/static/locales/en.json b/static/locales/en.json index 0572b956c..86ebc1126 100644 --- a/static/locales/en.json +++ b/static/locales/en.json @@ -460,5 +460,24 @@ "LBRY names cannot contain that symbol ($, #, @)": "LBRY names cannot contain that symbol ($, #, @)", "Path copied.": "Path copied.", "Open Folder": "Open Folder", - "Create Backup": "Create Backup" -} + "Create Backup": "Create Backup", + "Submit": "Submit", + "Website": "Website", + "aprettygoodsite.com": "aprettygoodsite.com", + "yourstruly@example.com": "yourstruly@example.com", + "Thumbnail source": "Thumbnail source", + "Thumbnail (400x400)": "Thumbnail (400x400)", + "https://example.com/image.png": "https://example.com/image.png", + "Cover source": "Cover source", + "Cover (1000x300)": "Cover (1000x300)", + "Editing": "Editing", + "Edit Your Channel": "Edit Your Channel", + "Editing Your Channel": "Editing Your Channel", + "We can explain... We know this page won't win any design awards, we have a cool idea for channel edits in the future. We just wanted to release a very very very basic version that just barely kinda works so people can use it.": "We can explain... We know this page won't win any design awards, we have a cool idea for channel edits in the future. We just wanted to release a very very very basic version that just barely kinda works so people can use it.", + "We can explain... \n\n We know this page won't win any design awards, we have a cool idea for channel edits in the future. We just wanted to release a very very very basic version that just barely kinda works so people can use it.": "We can explain... \n\n We know this page won't win any design awards, we have a cool idea for channel edits in the future. We just wanted to release a very very very basic version that just barely kinda works so people can use it.", + "We can explain...": "We can explain...", + "We know this page won't win any design awards, we have a cool idea for channel edits in the future. We just wanted to release a very very very basic version that just barely kinda works so people can use": "We know this page won't win any design awards, we have a cool idea for channel edits in the future. We just wanted to release a very very very basic version that just barely kinda works so people can use", + "We know this page won't win any design awards, we just wanted to release a very very very basic version that just barely kinda works so people can use": "We know this page won't win any design awards, we just wanted to release a very very very basic version that just barely kinda works so people can use", + "We know this page won't win any design awards, we just wanted to release a very very very basic version that just barely kinda works so people can use it right now. There is a much nicer version in the works.": "We know this page won't win any design awards, we just wanted to release a very very very basic version that just barely kinda works so people can use it right now. There is a much nicer version in the works.", + "We know this page won't win any design awards, we just wanted to release a very very very basic version that just barely kinda works so people can use it right now. There is a much nicer version being worked on.": "We know this page won't win any design awards, we just wanted to release a very very very basic version that just barely kinda works so people can use it right now. There is a much nicer version being worked on." +} \ No newline at end of file diff --git a/static/locales/pl.json b/static/locales/pl.json index 617233669..85fb7d220 100644 --- a/static/locales/pl.json +++ b/static/locales/pl.json @@ -286,5 +286,8 @@ "Multi-language support is brand new and incomplete. Switching your language may have unintended consequences.": "Multi-language support is brand new and incomplete. Switching your language may have unintended consequences.", "Wallet": "Wallet", "Home": "Home", - "Following": "Following" -} \ No newline at end of file + "Following": "Following", + "Update ready to install": "Update ready to install", + "Install now": "Install now", + "Edit": "Edit" +} diff --git a/yarn.lock b/yarn.lock index 4225997c4..1cc8f1644 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6646,9 +6646,9 @@ lazy-val@^1.0.3, lazy-val@^1.0.4: yargs "^13.2.2" zstd-codec "^0.1.1" -lbry-redux@lbryio/lbry-redux#9a676ee311d573b84d11f402d918aeee77be76e1: +lbry-redux@lbryio/lbry-redux#efccab44cb025a14fd81ec05ffca0314710c8529: version "0.0.1" - resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/9a676ee311d573b84d11f402d918aeee77be76e1" + resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/efccab44cb025a14fd81ec05ffca0314710c8529" dependencies: proxy-polyfill "0.1.6" reselect "^3.0.0"