diff --git a/package.json b/package.json index f04677671..3fd5f5d01 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "@datapunt/matomo-tracker-js": "^0.1.4", "@exponent/electron-cookies": "^2.0.0", "@hot-loader/react-dom": "^16.8", - "@lbry/components": "^4.2.2", + "@lbry/components": "^4.2.5", "@reach/menu-button": "0.7.4", "@reach/rect": "^0.2.1", "@reach/tabs": "^0.1.5", diff --git a/ui/component/button/view.jsx b/ui/component/button/view.jsx index bb2501957..bc35bec12 100644 --- a/ui/component/button/view.jsx +++ b/ui/component/button/view.jsx @@ -77,6 +77,7 @@ const Button = forwardRef((props: Props, ref: any) => { 'button--primary': button === 'primary', 'button--secondary': button === 'secondary', 'button--alt': button === 'alt', + 'button--danger': button === 'danger', 'button--inverse': button === 'inverse', 'button--close': button === 'close', 'button--disabled': disabled, diff --git a/ui/component/channelEdit/view.jsx b/ui/component/channelEdit/view.jsx index 9defac559..60a768b2e 100644 --- a/ui/component/channelEdit/view.jsx +++ b/ui/component/channelEdit/view.jsx @@ -1,16 +1,18 @@ // @flow -import React, { useState, useEffect } from 'react'; +import * as MODALS from 'constants/modal_types'; +import * as ICONS from 'constants/icons'; +import React from 'react'; 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 * 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'; +import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; +import Card from 'component/common/card'; type Props = { claim: ChannelClaim, @@ -33,7 +35,10 @@ type Props = { createError: string, creatingChannel: boolean, onDone: () => void, - openModal: (id: string, { onUpdate: string => void, label: string, helptext: string, currentValue: string }) => void, + openModal: ( + id: string, + { onUpdate: string => void, assetName: string, helpText: string, currentValue: string, title: string } + ) => void, uri: string, }; @@ -59,51 +64,53 @@ function ChannelForm(props: Props) { createError, openModal, } = props; + const [params, setParams]: [any, (any) => void] = React.useState(getChannelParams()); + const [nameError, setNameError] = React.useState(undefined); + const [bidError, setBidError] = React.useState(''); const { claim_id: claimId } = claim || {}; - // fill this in with sdk data - const channelParams = { - website, - email, - cover, - thumbnail, - description, - title, - amount: 0.001, - languages: languages || [], - locations: locations || [], - tags: tags - ? tags.map(tag => { - return { name: tag }; - }) - : [], - }; - - 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; + const isNewChannel = !uri; - useEffect(() => { - let nameError; - if (!name && name !== undefined) { - nameError = __('A name is required for your url'); - } else if (!isNameValid(name, false)) { - nameError = INVALID_NAME_ERROR; + function getChannelParams() { + // fill this in with sdk data + const channelParams: { + website: string, + email: string, + cover: string, + thumbnail: string, + description: string, + title: string, + amount: number, + languages: ?Array, + locations: ?Array, + tags: ?Array<{ name: string }>, + claim_id?: string, + } = { + website, + email, + cover, + thumbnail, + description, + title, + amount: 0.001, + languages: languages || [], + locations: locations || [], + tags: tags + ? tags.map(tag => { + return { name: tag }; + }) + : [], + }; + + if (claimId) { + channelParams['claim_id'] = claimId; } - 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. + return channelParams; + } - const handleBidChange = (bid: number) => { + function handleBidChange(bid: number) { const { balance, amount } = props; const totalAvailableBidAmount = parseFloat(amount) || 0.0 + parseFloat(balance) || 0.0; setParams({ ...params, amount: bid }); @@ -119,17 +126,17 @@ function ChannelForm(props: Props) { } else { setBidError(''); } - }; + } - const handleThumbnailChange = (thumbnailUrl: string) => { + function handleThumbnailChange(thumbnailUrl: string) { setParams({ ...params, thumbnail: thumbnailUrl }); - }; + } - const handleCoverChange = (coverUrl: string) => { + function handleCoverChange(coverUrl: string) { setParams({ ...params, cover: coverUrl }); - }; + } - const handleSubmit = () => { + function handleSubmit() { if (uri) { updateChannel(params).then(success => { if (success) { @@ -143,28 +150,34 @@ function ChannelForm(props: Props) { } }); } - }; + } + + 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]); // TODO clear and bail after submit return ( <>
-
- {uri || `lbry://@${params.name || '...'}`} - {uri && ( -
- -
- )} -
+
+
-
-
- {!uri && ( - setParams({ ...params, name: e.target.value })} + + + + {__('General')} + {__('LBC Details')} + {__('Tags')} + {__('Other')} + + + + + + + +
@
+
+ + setParams({ ...params, name: e.target.value })} + /> +
+ {!isNewChannel && {__('This field cannot be changed.')}} + + setParams({ ...params, title: e.target.value })} + /> + setParams({ ...params, description: text })} + textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION} + /> + + } /> - )} - - 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) }); - } - }); - }} + + + handleBidChange(parseFloat(event.target.value))} + placeholder={0.1} + helper={__('Increasing your deposit can help your channel be discovered more easily.')} + /> + } /> -
-
-
- {updateError || createError ? ( - {updateError || createError} - ) : ( -

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

- )} -
-
+ + + + setParams({ ...params, website: e.target.value })} + /> + setParams({ ...params, email: e.target.value })} + /> + + } + /> + + + + + +
+
+ {updateError || createError ? ( + {updateError || createError} + ) : ( +

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

+ )} + {!isNewChannel && ( +
+ +
+ )} + + } + />
); diff --git a/ui/component/channelThumbnail/view.jsx b/ui/component/channelThumbnail/view.jsx index f378e1597..499353f93 100644 --- a/ui/component/channelThumbnail/view.jsx +++ b/ui/component/channelThumbnail/view.jsx @@ -1,5 +1,5 @@ // @flow -import React, { useState } from 'react'; +import React from 'react'; import { parseURI } from 'lbry-redux'; import classnames from 'classnames'; import Gerbil from './gerbil.png'; @@ -25,20 +25,13 @@ function ChannelThumbnail(props: Props) { small = false, allowGifs = false, } = props; + const [thumbError, setThumbError] = React.useState(false); const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://'); const thumbnailPreview = rawThumbnailPreview && rawThumbnailPreview.trim().replace(/^http:\/\//i, 'https://'); const channelThumbnail = thumbnail || thumbnailPreview; - - const [thumbError, setThumbError] = useState(false); - if (channelThumbnail && channelThumbnail.endsWith('gif') && !allowGifs) { - return ; - } - const showThumb = (!obscure && !!thumbnail) || thumbnailPreview; - // Generate a random color class based on the first letter of the channel name const { channelName } = parseURI(uri); - let initializer; let colorClassName; if (channelName) { @@ -47,6 +40,11 @@ function ChannelThumbnail(props: Props) { } else { colorClassName = `channel-thumbnail__default--4`; } + + if (channelThumbnail && channelThumbnail.endsWith('gif') && !allowGifs) { + return ; + } + return (
; + return ( + - - {uploadStatus} -
-
- )} - - - - - + ) : ( + { + if (file.name) { + setFileSelected(file); + // file.path is undefined in web but available in electron + setPathSelected(file.name || file.path); + } + }} + accept={accept} + /> + )} + +
+ {onDone && ( +
+ + } + /> ); } diff --git a/ui/component/youtubeTransferStatus/view.jsx b/ui/component/youtubeTransferStatus/view.jsx index 114400e74..87bc2f010 100644 --- a/ui/component/youtubeTransferStatus/view.jsx +++ b/ui/component/youtubeTransferStatus/view.jsx @@ -80,88 +80,82 @@ export default function YoutubeTransferStatus(props: Props) { return ( hasChannels && !isYoutubeTransferComplete && ( -
- 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')} - subtitle={ - - {hasPendingTransfers && - __('Your videos are currently being transferred. There is nothing else for you to do.')} - {transferEnabled && !hasPendingTransfers && __('Your videos are ready to be transferred.')} - {!transferEnabled && !hasPendingTransfers && __('Please check back later.')} - - } - body={ -
- {youtubeChannels.map((channel, index) => { - const { - lbry_channel_name: channelName, - channel_claim_id: claimId, - status_token: statusToken, - } = channel; - const url = buildURI({ channelName, channelClaimId: claimId }); - const transferState = getMessage(channel); - return ( -
- {claimId ? ( - {transferState}} - properties={false} - /> - ) : ( -
-

- - %channelName% is not yet ready to be transferred. Please allow up to one week, though it is - frequently faster. - -

-

- , - faqLink:

- )} -
- ); - })} - {videosImported && ( -
{__('%complete% / %total% videos transferred', { complete, total })}
- )} -
- } - actions={ - transferEnabled ? ( -
-
- ) : !hideChannelLink ? ( -
-
- ) : ( - false - ) - } - /> -
+ 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')} + subtitle={ + + {hasPendingTransfers && + __('Your videos are currently being transferred. There is nothing else for you to do.')} + {transferEnabled && !hasPendingTransfers && __('Your videos are ready to be transferred.')} + {!transferEnabled && !hasPendingTransfers && __('Please check back later.')} + + } + body={ +
+ {youtubeChannels.map((channel, index) => { + const { lbry_channel_name: channelName, channel_claim_id: claimId, status_token: statusToken } = channel; + const url = buildURI({ channelName, channelClaimId: claimId }); + const transferState = getMessage(channel); + return ( +
+ {claimId ? ( + {transferState}} + properties={false} + /> + ) : ( +
+

+ + %channelName% is not yet ready to be transferred. Please allow up to one week, though it is + frequently faster. + +

+

+ , + faqLink:

+ )} +
+ ); + })} + {videosImported && ( +
{__('%complete% / %total% videos transferred', { complete, total })}
+ )} +
+ } + actions={ + transferEnabled ? ( +
+
+ ) : !hideChannelLink ? ( +
+
+ ) : ( + false + ) + } + /> ) ); } diff --git a/ui/modal/modalImageUpload/view.jsx b/ui/modal/modalImageUpload/view.jsx index abc4972ec..dc0ad52a2 100644 --- a/ui/modal/modalImageUpload/view.jsx +++ b/ui/modal/modalImageUpload/view.jsx @@ -1,41 +1,31 @@ // @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, + title: string, + helpText: string, onUpdate: string => void, + assetName: string, }; -const ModalImageUpload = (props: Props) => { - const { closeModal, currentValue, label, helptext, onUpdate } = props; +function ModalImageUpload(props: Props) { + const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props; return ( - - onUpdate(v)} - currentValue={currentValue} - assetName={label} - recommended={__(helptext)} - /> - } - actions={ -
-
- } + + onUpdate(v)} + currentValue={currentValue} + assetName={assetName} + recommended={helpText} + onDone={closeModal} /> ); -}; +} export default ModalImageUpload; diff --git a/ui/page/channel/view.jsx b/ui/page/channel/view.jsx index 847930322..8b0d3f7f9 100644 --- a/ui/page/channel/view.jsx +++ b/ui/page/channel/view.jsx @@ -1,6 +1,6 @@ // @flow import * as ICONS from 'constants/icons'; -import React, { useState, useEffect } from 'react'; +import React from 'react'; import { parseURI } from 'lbry-redux'; import { Lbryio } from 'lbryinc'; import Page from 'component/page'; @@ -8,7 +8,7 @@ import SubscribeButton from 'component/subscribeButton'; import BlockButton from 'component/blockButton'; import ShareButton from 'component/shareButton'; import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; -import { withRouter } from 'react-router'; +import { useHistory } from 'react-router'; import Button from 'component/button'; import { formatLbryUrlForWeb } from 'util/url'; import ChannelContent from 'component/channelContent'; @@ -26,6 +26,7 @@ import ClaimSupportButton from 'component/claimSupportButton'; const PAGE_VIEW_QUERY = `view`; const ABOUT_PAGE = `about`; const DISCUSSION_PAGE = `discussion`; +const EDIT_PAGE = 'edit'; type Props = { uri: string, @@ -34,8 +35,6 @@ type Props = { cover: ?string, thumbnail: ?string, page: number, - location: { search: string }, - history: { push: string => void }, match: { params: { attribute: ?string } }, channelIsMine: boolean, isSubscribed: boolean, @@ -55,8 +54,6 @@ function ChannelPage(props: Props) { claim, title, cover, - history, - location, page, channelIsMine, isSubscribed, @@ -66,16 +63,27 @@ function ChannelPage(props: Props) { subCount, pending, } = props; - - const { channelName } = parseURI(uri); - const { search } = location; + const { + push, + goBack, + location: { search }, + } = useHistory(); const urlParams = new URLSearchParams(search); const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined; + const editInUrl = urlParams.get(PAGE_VIEW_QUERY) === EDIT_PAGE; + const [editing, setEditing] = React.useState(editInUrl); + const [lastYtSyncDate, setLastYtSyncDate] = React.useState(); + const { channelName } = parseURI(uri); const { permanent_url: permanentUrl } = claim; - const [editing, setEditing] = useState(false); - const [lastYtSyncDate, setLastYtSyncDate] = useState(); const claimId = claim.claim_id; const formattedSubCount = Number(subCount).toLocaleString(); + let channelIsBlackListed = false; + + if (claim && blackListedOutpoints) { + channelIsBlackListed = blackListedOutpoints.some( + outpoint => outpoint.txid === claim.txid && outpoint.nout === claim.nout + ); + } // 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 @@ -93,14 +101,34 @@ function ChannelPage(props: Props) { } else { search += `${PAGE_VIEW_QUERY}=${DISCUSSION_PAGE}`; } - history.push(`${url}${search}`); + + push(`${url}${search}`); } function onDone() { setEditing(false); + goBack(); } - useEffect(() => { + React.useEffect(() => { + if (!channelIsMine && editing) { + setEditing(false); + } + + if (channelIsMine && editing) { + push(`?${PAGE_VIEW_QUERY}=${EDIT_PAGE}`); + } + }, [channelIsMine, editing, push]); + + React.useEffect(() => { + if (currentView === EDIT_PAGE) { + setEditing(true); + } else { + setEditing(false); + } + }, [currentView, setEditing]); + + React.useEffect(() => { Lbryio.call('yt', 'get_youtuber', { channel_claim_id: claimId }).then(response => { if (response.is_verified_youtuber) { setLastYtSyncDate(response.last_synced); @@ -108,31 +136,20 @@ function ChannelPage(props: Props) { }); }, [claimId]); - let channelIsBlackListed = false; - - if (claim && blackListedOutpoints) { - channelIsBlackListed = blackListedOutpoints.some( - outpoint => outpoint.txid === claim.txid && outpoint.nout === claim.nout - ); - } - React.useEffect(() => { fetchSubCount(claimId); }, [uri, fetchSubCount, claimId]); - React.useEffect(() => { - if (!channelIsMine && editing) { - setEditing(false); - } - }, [channelIsMine, editing]); - if (editing) { return ( @@ -219,4 +236,4 @@ function ChannelPage(props: Props) { ); } -export default withRouter(ChannelPage); +export default ChannelPage; diff --git a/ui/page/channels/view.jsx b/ui/page/channels/view.jsx index 71d9fd79f..d02ea7d6e 100644 --- a/ui/page/channels/view.jsx +++ b/ui/page/channels/view.jsx @@ -30,25 +30,28 @@ export default function ChannelsPage(props: Props) { return ( - {hasYoutubeChannels && } +
+ {hasYoutubeChannels && } + + {channelUrls && Boolean(channelUrls.length) && ( + +
- {channelUrls && Boolean(channelUrls.length) && ( - -