Merge pull request #4445 from lbryio/feat-newChannelCreate

new channel creating and editing
This commit is contained in:
jessopb 2020-07-03 11:09:25 -04:00 committed by GitHub
commit 70c6034662
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1112 additions and 675 deletions

View file

@ -70,7 +70,7 @@
"@datapunt/matomo-tracker-js": "^0.1.4", "@datapunt/matomo-tracker-js": "^0.1.4",
"@exponent/electron-cookies": "^2.0.0", "@exponent/electron-cookies": "^2.0.0",
"@hot-loader/react-dom": "^16.8", "@hot-loader/react-dom": "^16.8",
"@lbry/components": "^4.2.2", "@lbry/components": "^4.2.5",
"@reach/menu-button": "0.7.4", "@reach/menu-button": "0.7.4",
"@reach/rect": "^0.2.1", "@reach/rect": "^0.2.1",
"@reach/tabs": "^0.1.5", "@reach/tabs": "^0.1.5",
@ -135,7 +135,7 @@
"imagesloaded": "^4.1.4", "imagesloaded": "^4.1.4",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"lbry-format": "https://github.com/lbryio/lbry-format.git", "lbry-format": "https://github.com/lbryio/lbry-format.git",
"lbry-redux": "lbryio/lbry-redux#eb47b7e5b6cc24db93b2b66cf1153b02858caf58", "lbry-redux": "lbryio/lbry-redux#906199d866a187015668a27363f010828c15979a",
"lbryinc": "lbryio/lbryinc#72eee35f5181940eb4a468a27ddb2a2a4e362fb0", "lbryinc": "lbryio/lbryinc#72eee35f5181940eb4a468a27ddb2a2a4e362fb0",
"lint-staged": "^7.0.2", "lint-staged": "^7.0.2",
"localforage": "^1.7.1", "localforage": "^1.7.1",

View file

@ -1260,5 +1260,34 @@
"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.", "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.", "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", "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?" "Are you sure you want to remove this from the list?": "Are you sure you want to remove this from the list?",
"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",
"CableTube Escape Artists": "CableTube Escape Artists",
"General": "General",
"MyAwesomeChannel": "MyAwesomeChannel",
"My Awesome Channel": "My Awesome Channel",
"Increasing your deposit can help your channel be discovered more easily.": "Increasing your deposit can help your channel be discovered more easily.",
"Editing @%channel%": "Editing @%channel%",
"This field cannot be changed.": "This field cannot be changed.",
"Delete Channel": "Delete Channel",
"Edit Thumbnail Image": "Edit Thumbnail Image",
"(Y x Z)": "(Y x Z)",
"Choose Image": "Choose Image",
"File to upload": "File to upload",
"Use a URL instead": "Use a URL instead",
"Edit Cover Image": "Edit Cover Image",
"Cover Image": "Cover Image",
"You Followed Your First Channel!": "You Followed Your First Channel!",
"Awesome! You just followed your first first channel.": "Awesome! You just followed your first first channel.",
"After submitting, it will take a few minutes for your changes to be live for everyone.": "After submitting, it will take a few minutes for your changes to be live for everyone."
} }

View file

@ -31,11 +31,13 @@ function ChannelAbout(props: Props) {
<div className="card"> <div className="card">
<section className="section card--section"> <section className="section card--section">
<Fragment> <Fragment>
<label>{__('Description')}</label>
{description && ( {description && (
<div className="media__info-text media__info-text--constrained"> <>
<MarkdownPreview content={description} /> <label>{__('Description')}</label>
</div> <div className="media__info-text media__info-text--constrained">
<MarkdownPreview content={description} />
</div>
</>
)} )}
{email && ( {email && (
<Fragment> <Fragment>

View file

@ -28,7 +28,7 @@ class ChannelCreate extends React.PureComponent<Props, State> {
this.state = { this.state = {
newChannelName: '', newChannelName: '',
newChannelBid: 0.01, newChannelBid: 0.001,
newChannelNameError: '', newChannelNameError: '',
newChannelBidError: '', newChannelBidError: '',
}; };

View file

@ -3,21 +3,27 @@ import {
makeSelectTitleForUri, makeSelectTitleForUri,
makeSelectThumbnailForUri, makeSelectThumbnailForUri,
makeSelectCoverForUri, makeSelectCoverForUri,
selectCurrentChannelPage,
makeSelectMetadataItemForUri, makeSelectMetadataItemForUri,
doUpdateChannel, doUpdateChannel,
doCreateChannel,
makeSelectAmountForUri, makeSelectAmountForUri,
makeSelectClaimForUri, makeSelectClaimForUri,
selectUpdateChannelError, selectUpdateChannelError,
selectUpdatingChannel, selectUpdatingChannel,
selectCreateChannelError,
selectCreatingChannel,
selectBalance,
doClearChannelErrors,
} from 'lbry-redux'; } from 'lbry-redux';
import { doOpenModal } from 'redux/actions/app';
import ChannelPage from './view'; import ChannelPage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
title: makeSelectTitleForUri(props.uri)(state), title: makeSelectTitleForUri(props.uri)(state),
thumbnailUrl: makeSelectThumbnailForUri(props.uri)(state), thumbnailUrl: makeSelectThumbnailForUri(props.uri)(state),
coverUrl: makeSelectCoverForUri(props.uri)(state), coverUrl: makeSelectCoverForUri(props.uri)(state),
page: selectCurrentChannelPage(state),
description: makeSelectMetadataItemForUri(props.uri, 'description')(state), description: makeSelectMetadataItemForUri(props.uri, 'description')(state),
website: makeSelectMetadataItemForUri(props.uri, 'website_url')(state), website: makeSelectMetadataItemForUri(props.uri, 'website_url')(state),
email: makeSelectMetadataItemForUri(props.uri, 'email')(state), email: makeSelectMetadataItemForUri(props.uri, 'email')(state),
@ -25,13 +31,21 @@ const select = (state, props) => ({
locations: makeSelectMetadataItemForUri(props.uri, 'locations')(state), locations: makeSelectMetadataItemForUri(props.uri, 'locations')(state),
languages: makeSelectMetadataItemForUri(props.uri, 'languages')(state), languages: makeSelectMetadataItemForUri(props.uri, 'languages')(state),
amount: makeSelectAmountForUri(props.uri)(state), amount: makeSelectAmountForUri(props.uri)(state),
claim: makeSelectClaimForUri(props.uri)(state),
updateError: selectUpdateChannelError(state), updateError: selectUpdateChannelError(state),
updatingChannel: selectUpdatingChannel(state), updatingChannel: selectUpdatingChannel(state),
createError: selectCreateChannelError(state),
creatingChannel: selectCreatingChannel(state),
balance: selectBalance(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
updateChannel: params => dispatch(doUpdateChannel(params)), updateChannel: params => dispatch(doUpdateChannel(params)),
createChannel: params => {
const { name, amount, ...optionalParams } = params;
return dispatch(doCreateChannel('@' + name, amount, optionalParams));
},
clearChannelErrors: () => dispatch(doClearChannelErrors()),
}); });
export default connect(select, perform)(ChannelPage); export default connect(select, perform)(ChannelPage);

View file

@ -1,19 +1,29 @@
// @flow // @flow
import React, { useState } 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 { FormField } from 'component/common/form';
import Button from 'component/button'; import Button from 'component/button';
import SelectAsset from 'component/selectAsset';
import { MINIMUM_PUBLISH_BID } from 'constants/claim';
import TagsSearch from 'component/tagsSearch'; import TagsSearch from 'component/tagsSearch';
import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field'; import { FF_MAX_CHARS_IN_DESCRIPTION } from 'constants/form-field';
import ErrorText from 'component/common/error-text'; 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';
const MAX_TAG_SELECT = 5;
type Props = { type Props = {
claim: ChannelClaim, claim: ChannelClaim,
title: ?string, title: string,
amount: string, amount: number,
coverUrl: ?string, coverUrl: string,
thumbnailUrl: ?string, thumbnailUrl: string,
location: { search: string }, location: { search: string },
description: string, description: string,
website: string, website: string,
@ -23,205 +33,365 @@ type Props = {
locations: Array<string>, locations: Array<string>,
languages: Array<string>, languages: Array<string>,
updateChannel: any => Promise<any>, updateChannel: any => Promise<any>,
updateThumb: string => void,
updateCover: string => void,
doneEditing: () => void,
updateError: string,
updatingChannel: boolean, 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,
}; };
function ChannelForm(props: Props) { function ChannelForm(props: Props) {
const { const {
uri,
claim, claim,
amount,
title, title,
coverUrl,
description, description,
website, website,
email, email,
thumbnailUrl, thumbnailUrl,
coverUrl,
tags, tags,
locations, locations,
languages, languages,
amount, onDone,
doneEditing,
updateChannel, updateChannel,
updateThumb,
updateCover,
updateError, updateError,
updatingChannel, updatingChannel,
createChannel,
creatingChannel,
createError,
clearChannelErrors,
openModal,
} = props; } = props;
const { claim_id: claimId } = claim; const [nameError, setNameError] = React.useState(undefined);
const [bidError, setBidError] = React.useState('');
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();
// fill this in with sdk data function getChannelParams() {
const channelParams = { // fill this in with sdk data
website, const channelParams: {
email, website: string,
coverUrl, email: string,
thumbnailUrl, coverUrl: string,
description, thumbnailUrl: string,
title, description: string,
amount, title: string,
claim_id: claimId, amount: number,
languages: languages || [], languages: ?Array<string>,
locations: locations || [], locations: ?Array<string>,
tags: tags tags: ?Array<{ name: string }>,
? tags.map(tag => { claim_id?: string,
return { name: tag }; } = {
}) website,
: [], email,
}; coverUrl,
thumbnailUrl,
description,
title,
amount: amount || 0.001,
languages: languages || [],
locations: locations || [],
tags: tags
? tags.map(tag => {
return { name: tag };
})
: [],
};
const [params, setParams] = useState(channelParams); if (claimId) {
const [bidError, setBidError] = useState(''); channelParams['claim_id'] = claimId;
}
// If a user changes tabs, update the url so it stays on the same page if they refresh. return channelParams;
// 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) => { function handleBidChange(bid: number) {
const { balance, amount } = props; const { balance, amount } = props;
const totalAvailableBidAmount = parseFloat(amount) + parseFloat(balance); const totalAvailableBidAmount = (parseFloat(amount) || 0.0) + (parseFloat(balance) || 0.0);
setParams({ ...params, amount: bid }); setParams({ ...params, amount: bid });
setBidError('');
if (bid <= 0.0 || isNaN(bid)) { if (bid <= 0.0 || isNaN(bid)) {
setBidError(__('Deposit cannot be 0')); setBidError(__('Deposit cannot be 0'));
} else if (totalAvailableBidAmount === bid) {
setBidError(__('Please decrease your deposit to account for transaction fees'));
} else if (totalAvailableBidAmount < bid) { } else if (totalAvailableBidAmount < bid) {
setBidError(__('Deposit cannot be higher than your balance')); setBidError(__('Deposit cannot be higher than your balance'));
} else if (totalAvailableBidAmount - bid < ESTIMATED_FEE) {
setBidError(__('Please decrease your deposit to account for transaction fees'));
} else if (bid < MINIMUM_PUBLISH_BID) { } else if (bid < MINIMUM_PUBLISH_BID) {
setBidError(__('Your deposit must be higher')); setBidError(__('Your deposit must be higher'));
} else {
setBidError('');
} }
}; }
const handleThumbnailChange = (thumbnailUrl: string) => { function handleThumbnailChange(thumbnailUrl: string) {
setParams({ ...params, thumbnailUrl }); setParams({ ...params, thumbnailUrl });
updateThumb(thumbnailUrl); }
};
const handleCoverChange = (coverUrl: string) => { function handleCoverChange(coverUrl: string) {
setParams({ ...params, coverUrl }); setParams({ ...params, coverUrl });
updateCover(coverUrl); }
};
const handleSubmit = () => { function handleSubmit() {
updateChannel(params).then(success => { if (uri) {
if (success) { updateChannel(params).then(success => {
doneEditing(); if (success) {
} onDone();
}); }
}; });
} else {
createChannel(params).then(success => {
if (success) {
analytics.apiLogPublish(success);
onDone();
}
});
}
}
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 // TODO clear and bail after submit
return ( return (
<div className="card"> <>
<section className={'section card--section'}> <div className="main--contained">
<SelectAsset <header className="channel-cover">
onUpdate={v => handleThumbnailChange(v)} <div className="channel__quick-actions">
currentValue={params.thumbnailUrl} <Button
assetName={'Thumbnail'} button="alt"
recommended={__('Recommended ratio is 1:1')} title={__('Cover')}
/> onClick={() =>
openModal(MODALS.IMAGE_UPLOAD, {
<SelectAsset onUpdate: coverUrl => handleCoverChange(coverUrl),
onUpdate={v => handleCoverChange(v)} title: __('Edit Cover Image'),
currentValue={params.coverUrl} helpText: __('(6.25:1)'),
assetName={'Cover'} assetName: __('Cover Image'),
recommended={__('Recommended ratio is 6.25:1')} currentValue: params.coverUrl,
/> })
<FormField
type="text"
name="channel_title2"
label={__('Title')}
placeholder={__('Titular Title')}
disabled={false}
value={params.title}
onChange={e => setParams({ ...params, title: e.target.value })}
/>
<FormField
className="form-field--price-amount"
type="number"
name="content_bid2"
step="any"
label={__('Deposit (LBC)')}
postfix="LBC"
value={params.amount}
error={bidError}
min="0.0"
disabled={false}
onChange={event => handleBidChange(parseFloat(event.target.value))}
placeholder={0.1}
/>
<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
type="markdown"
name="content_description2"
label={__('Description')}
placeholder={__('Description of your content')}
value={params.description}
disabled={false}
onChange={text => setParams({ ...params, description: text })}
textAreaMaxLength={FF_MAX_CHARS_IN_DESCRIPTION}
/>
<TagsSearch
suggestMature
disableAutoFocus
tagsPassedIn={params.tags || []}
label={__('Tags Selected')}
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) });
} }
}); icon={ICONS.CAMERA}
}} iconSize={18}
/>
</div>
{params.coverUrl && <img className="channel-cover__custom" src={params.coverUrl} />}
<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
/>
<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>{__('LBC 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={__('Deposit (LBC)')}
postfix="LBC"
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.')}
/>
}
/>
</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
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 })}
/>
</>
}
/>
</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>
{updateError || createError ? (
<ErrorText>{updateError || createError}</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 className={'section__actions'}> </div>
<Button </>
button="primary"
label={updatingChannel ? __('Submitting...') : __('Submit')}
onClick={handleSubmit}
/>
<Button button="link" label={__('Cancel')} onClick={doneEditing} />
</div>
{updateError && updateError.length ? (
<ErrorText>{updateError}</ErrorText>
) : (
<p className="help">
{__('After submitting, you will not see the changes immediately. Please check back in a few minutes.')}
</p>
)}
</section>
</div>
); );
} }

View file

@ -1,5 +1,5 @@
// @flow // @flow
import React, { useState } from 'react'; import React from 'react';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import classnames from 'classnames'; import classnames from 'classnames';
import Gerbil from './gerbil.png'; import Gerbil from './gerbil.png';
@ -25,20 +25,13 @@ function ChannelThumbnail(props: Props) {
small = false, small = false,
allowGifs = false, allowGifs = false,
} = props; } = props;
const [thumbError, setThumbError] = React.useState(false);
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://'); const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
const thumbnailPreview = rawThumbnailPreview && rawThumbnailPreview.trim().replace(/^http:\/\//i, 'https://'); const thumbnailPreview = rawThumbnailPreview && rawThumbnailPreview.trim().replace(/^http:\/\//i, 'https://');
const channelThumbnail = thumbnail || thumbnailPreview; const channelThumbnail = thumbnail || thumbnailPreview;
const [thumbError, setThumbError] = useState(false);
if (channelThumbnail && channelThumbnail.endsWith('gif') && !allowGifs) {
return <FreezeframeWrapper src={channelThumbnail} className="channel-thumbnail" />;
}
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview; const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
// Generate a random color class based on the first letter of the channel name // Generate a random color class based on the first letter of the channel name
const { channelName } = parseURI(uri); const { channelName } = parseURI(uri);
let initializer; let initializer;
let colorClassName; let colorClassName;
if (channelName) { if (channelName) {
@ -47,6 +40,11 @@ function ChannelThumbnail(props: Props) {
} else { } else {
colorClassName = `channel-thumbnail__default--4`; colorClassName = `channel-thumbnail__default--4`;
} }
if (channelThumbnail && channelThumbnail.endsWith('gif') && !allowGifs) {
return <FreezeframeWrapper src={channelThumbnail} className="channel-thumbnail" />;
}
return ( return (
<div <div
className={classnames('channel-thumbnail', className, { className={classnames('channel-thumbnail', className, {

View file

@ -12,11 +12,20 @@ type Props = {
}; };
export default function ClaimAbandonButton(props: Props) { export default function ClaimAbandonButton(props: Props) {
const { doOpenModal, claim, abandonActionCallback, iconSize } = props; const { doOpenModal, claim, abandonActionCallback } = props;
function abandonClaim() { function abandonClaim() {
doOpenModal(MODALS.CONFIRM_CLAIM_REVOKE, { claim: claim, cb: abandonActionCallback }); doOpenModal(MODALS.CONFIRM_CLAIM_REVOKE, { claim: claim, cb: abandonActionCallback });
} }
return <Button disabled={!claim} button="alt" iconSize={iconSize} icon={ICONS.DELETE} onClick={abandonClaim} />; return (
<Button
disabled={!claim}
label={__('Delete Channel')}
button="alt"
iconColor="red"
icon={ICONS.DELETE}
onClick={abandonClaim}
/>
);
} }

View file

@ -15,9 +15,14 @@ type Props = {
accept?: string, accept?: string,
error?: string, error?: string,
disabled?: boolean, disabled?: boolean,
autoFocus?: boolean,
}; };
class FileSelector extends React.PureComponent<Props> { class FileSelector extends React.PureComponent<Props> {
static defaultProps = {
autoFocus: false,
};
componentDidUpdate(prevProps: Props) { componentDidUpdate(prevProps: Props) {
// If the form has just been cleared, // If the form has just been cleared,
// clear the file input // clear the file input
@ -58,7 +63,18 @@ class FileSelector extends React.PureComponent<Props> {
input: ?HTMLInputElement; input: ?HTMLInputElement;
render() { render() {
const { type, currentPath, label, fileLabel, directoryLabel, placeholder, accept, error, disabled } = this.props; const {
type,
currentPath,
label,
fileLabel,
directoryLabel,
placeholder,
accept,
error,
disabled,
autoFocus = false,
} = this.props;
const buttonLabel = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory'); const buttonLabel = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory');
const placeHolder = currentPath || placeholder; const placeHolder = currentPath || placeholder;
@ -74,7 +90,13 @@ class FileSelector extends React.PureComponent<Props> {
readOnly="readonly" readOnly="readonly"
value={placeHolder || __('Choose a file')} value={placeHolder || __('Choose a file')}
inputButton={ inputButton={
<Button button="secondary" disabled={disabled} onClick={this.fileInputButton} label={buttonLabel} /> <Button
autoFocus={autoFocus}
button="secondary"
disabled={disabled}
onClick={this.fileInputButton}
label={buttonLabel}
/>
} }
/> />
<input <input

View file

@ -662,7 +662,9 @@ export const icons = {
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" /> <path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z" />
</g> </g>
), ),
[ICONS.OPEN_LOG_FOLDER]: buildIcon(<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />), [ICONS.OPEN_LOG_FOLDER]: buildIcon(
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
),
[ICONS.OPEN_LOG]: buildIcon( [ICONS.OPEN_LOG]: buildIcon(
<g> <g>
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
@ -671,4 +673,10 @@ export const icons = {
<line x1="9" y1="15" x2="15" y2="15" /> <line x1="9" y1="15" x2="15" y2="15" />
</g> </g>
), ),
[ICONS.CAMERA]: buildIcon(
<g>
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
<circle cx="12" cy="13" r="4" />
</g>
),
}; };

View file

@ -13,6 +13,7 @@ import { Menu, MenuList, MenuButton, MenuItem } from '@reach/menu-button';
import Tooltip from 'component/common/tooltip'; import Tooltip from 'component/common/tooltip';
import NavigationButton from 'component/navigationButton'; import NavigationButton from 'component/navigationButton';
import { LOGO_TITLE } from 'config'; import { LOGO_TITLE } from 'config';
import useIsMobile from 'effects/use-is-mobile';
// @if TARGET='app' // @if TARGET='app'
import { remote } from 'electron'; import { remote } from 'electron';
import { IS_MAC } from 'component/app/view'; import { IS_MAC } from 'component/app/view';
@ -37,6 +38,11 @@ type Props = {
email: ?string, email: ?string,
authenticated: boolean, authenticated: boolean,
authHeader: boolean, authHeader: boolean,
backout: {
backFunction: () => void,
title: string,
simpleTitle: string, // Just use the same value as `title` if `title` is already short (~< 10 chars), unless you have a better idea for title overlfow on mobile
},
syncError: ?string, syncError: ?string,
emailToVerify?: string, emailToVerify?: string,
signOut: () => void, signOut: () => void,
@ -61,13 +67,13 @@ const Header = (props: Props) => {
signOut, signOut,
syncError, syncError,
openMobileNavigation, openMobileNavigation,
openChannelCreate,
openSignOutModal, openSignOutModal,
clearEmailEntry, clearEmailEntry,
clearPasswordEntry, clearPasswordEntry,
emailToVerify, emailToVerify,
backout,
} = props; } = props;
const isMobile = useIsMobile();
// on the verify page don't let anyone escape other than by closing the tab to keep session data consistent // on the verify page don't let anyone escape other than by closing the tab to keep session data consistent
const isVerifyPage = history.location.pathname.includes(PAGES.AUTH_VERIFY); const isVerifyPage = history.location.pathname.includes(PAGES.AUTH_VERIFY);
const isSignUpPage = history.location.pathname.includes(PAGES.AUTH); const isSignUpPage = history.location.pathname.includes(PAGES.AUTH);
@ -135,201 +141,223 @@ const Header = (props: Props) => {
// @endif // @endif
> >
<div className="header__contents"> <div className="header__contents">
<div className="header__navigation"> {!authHeader && backout ? (
<Button <div className="card__actions--between">
className="header__navigation-item header__navigation-item--lbry header__navigation-item--button-mobile" <Button onClick={backout.backFunction} button="link" label={__('Cancel')} icon={ICONS.ARROW_LEFT} />
label={LOGO_TITLE} {backout.title && (
icon={ICONS.LBRY} <h1 className={'card__title'}>{isMobile ? backout.simpleTitle || backout.title : backout.title}</h1>
onClick={() => {
if (history.location.pathname === '/') window.location.reload();
}}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
{...homeButtonNavigationProps}
/>
{/* @if TARGET='app' */}
{!authHeader && (
<div className="header__navigation-arrows">
<NavigationButton isBackward history={history} />
<NavigationButton isBackward={false} history={history} />
</div>
)}
{/* @endif */}
{!authHeader && <WunderBar />}
</div>
{!authHeader ? (
<div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}>
{(!IS_WEB || authenticated) && (
<Fragment>
<Button
aria-label={__('Your wallet')}
navigate={`/$/${PAGES.WALLET}`}
className="header__navigation-item menu__title header__navigation-item--balance"
label={getWalletTitle()}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
<Menu>
<MenuButton
aria-label={__('Publish a file, or create a channel')}
title={__('Publish a file, or create a channel')}
className="header__navigation-item menu__title header__navigation-item--icon"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.PUBLISH} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publish')}
</MenuItem>
<MenuItem className="menu__link" onSelect={openChannelCreate}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('New Channel')}
</MenuItem>
</MenuList>
</Menu>
<Menu>
<MenuButton
aria-label={__('Your account')}
title={__('Your account')}
className="header__navigation-item menu__title header__navigation-item--icon"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISHED}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publishes')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNELS}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('Channels')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CREATOR_DASHBOARD}`)}>
<Icon aria-hidden icon={ICONS.ANALYTICS} />
{__('Creator Analytics')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.REWARDS} />
{__('Rewards')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.INVITE}`)}>
<Icon aria-hidden icon={ICONS.INVITE} />
{__('Invites')}
</MenuItem>
{authenticated ? (
<MenuItem onSelect={IS_WEB ? signOut : openSignOutModal}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
</div>
<span className="menu__link-help">{email}</span>
</MenuItem>
) : (
<React.Fragment>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
<Icon aria-hidden icon={ICONS.SIGN_UP} />
{__('Register')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH_SIGNIN}`)}>
<Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')}
</MenuItem>
</React.Fragment>
)}
</MenuList>
</Menu>
</Fragment>
)} )}
<Menu> <Button
<MenuButton aria-label={__('Your wallet')}
aria-label={__('Settings')} navigate={`/$/${PAGES.WALLET}`}
title={__('Settings')} className="header__navigation-item menu__title header__navigation-item--balance"
className="header__navigation-item menu__title header__navigation-item--icon" label={getWalletTitle()}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
</div>
) : (
<>
<div className="header__navigation">
<Button
className="header__navigation-item header__navigation-item--lbry header__navigation-item--button-mobile"
label={LOGO_TITLE}
icon={ICONS.LBRY}
onClick={() => {
if (history.location.pathname === '/') window.location.reload();
}}
// @if TARGET='app' // @if TARGET='app'
onDoubleClick={e => { onDoubleClick={e => {
e.stopPropagation(); e.stopPropagation();
}} }}
// @endif // @endif
> {...homeButtonNavigationProps}
<Icon size={18} icon={ICONS.SETTINGS} aria-hidden /> />
</MenuButton>
<MenuList className="menu__list--header"> {/* @if TARGET='app' */}
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.SETTINGS}`)}> {!authHeader && (
<Icon aria-hidden tootlip icon={ICONS.SETTINGS} /> <div className="header__navigation-arrows">
{__('Settings')} <NavigationButton isBackward history={history} />
</MenuItem> <NavigationButton isBackward={false} history={history} />
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.HELP}`)}> </div>
<Icon aria-hidden icon={ICONS.HELP} /> )}
{__('Help')} {/* @endif */}
</MenuItem>
<MenuItem className="menu__link" onSelect={handleThemeToggle}> {!authHeader && <WunderBar />}
<Icon icon={currentTheme === 'light' ? ICONS.DARK : ICONS.LIGHT} />
{currentTheme === 'light' ? __('Dark') : __('Light')}
</MenuItem>
</MenuList>
</Menu>
{IS_WEB && !authenticated && (
<div className="header__auth-buttons">
<Button navigate={`/$/${PAGES.AUTH_SIGNIN}`} button="link" label={__('Sign In')} />
<Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Register')} />
</div>
)}
</div>
) : (
!isVerifyPage && (
<div className="header__menu">
{/* Add an empty span here so we can use the same style as above */}
{/* This pushes the close button to the right side */}
<span />
<Tooltip label={__('Go Back')}>
<Button
button="alt"
// className="button--header-close"
icon={ICONS.REMOVE}
{...closeButtonNavigationProps}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
</Tooltip>
</div> </div>
)
{!authHeader ? (
<div className={classnames('header__menu', { 'header__menu--with-balance': !IS_WEB || authenticated })}>
{(!IS_WEB || authenticated) && (
<Fragment>
<Button
aria-label={__('Your wallet')}
navigate={`/$/${PAGES.WALLET}`}
className="header__navigation-item menu__title header__navigation-item--balance"
label={getWalletTitle()}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
<Menu>
<MenuButton
aria-label={__('Publish a file, or create a channel')}
title={__('Publish a file, or create a channel')}
className="header__navigation-item menu__title header__navigation-item--icon"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.PUBLISH} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISH}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publish')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNEL_NEW}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('New Channel')}
</MenuItem>
</MenuList>
</Menu>
<Menu>
<MenuButton
aria-label={__('Your account')}
title={__('Your account')}
className="header__navigation-item menu__title header__navigation-item--icon"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.ACCOUNT} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.PUBLISHED}`)}>
<Icon aria-hidden icon={ICONS.PUBLISH} />
{__('Publishes')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CHANNELS}`)}>
<Icon aria-hidden icon={ICONS.CHANNEL} />
{__('Channels')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.CREATOR_DASHBOARD}`)}>
<Icon aria-hidden icon={ICONS.ANALYTICS} />
{__('Creator Analytics')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.REWARDS}`)}>
<Icon aria-hidden icon={ICONS.REWARDS} />
{__('Rewards')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.INVITE}`)}>
<Icon aria-hidden icon={ICONS.INVITE} />
{__('Invites')}
</MenuItem>
{authenticated ? (
<MenuItem onSelect={IS_WEB ? signOut : openSignOutModal}>
<div className="menu__link">
<Icon aria-hidden icon={ICONS.SIGN_OUT} />
{__('Sign Out')}
</div>
<span className="menu__link-help">{email}</span>
</MenuItem>
) : (
<React.Fragment>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH}`)}>
<Icon aria-hidden icon={ICONS.SIGN_UP} />
{__('Register')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.AUTH_SIGNIN}`)}>
<Icon aria-hidden icon={ICONS.SIGN_IN} />
{__('Sign In')}
</MenuItem>
</React.Fragment>
)}
</MenuList>
</Menu>
</Fragment>
)}
<Menu>
<MenuButton
aria-label={__('Settings')}
title={__('Settings')}
className="header__navigation-item menu__title header__navigation-item--icon"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
>
<Icon size={18} icon={ICONS.SETTINGS} aria-hidden />
</MenuButton>
<MenuList className="menu__list--header">
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.SETTINGS}`)}>
<Icon aria-hidden tootlip icon={ICONS.SETTINGS} />
{__('Settings')}
</MenuItem>
<MenuItem className="menu__link" onSelect={() => history.push(`/$/${PAGES.HELP}`)}>
<Icon aria-hidden icon={ICONS.HELP} />
{__('Help')}
</MenuItem>
<MenuItem className="menu__link" onSelect={handleThemeToggle}>
<Icon icon={currentTheme === 'light' ? ICONS.DARK : ICONS.LIGHT} />
{currentTheme === 'light' ? __('Dark') : __('Light')}
</MenuItem>
</MenuList>
</Menu>
{IS_WEB && !authenticated && (
<div className="header__auth-buttons">
<Button navigate={`/$/${PAGES.AUTH_SIGNIN}`} button="link" label={__('Sign In')} />
<Button navigate={`/$/${PAGES.AUTH}`} button="primary" label={__('Register')} />
</div>
)}
</div>
) : (
!isVerifyPage && (
<div className="header__menu">
{/* Add an empty span here so we can use the same style as above */}
{/* This pushes the close button to the right side */}
<span />
<Tooltip label={__('Go Back')}>
<Button
button="alt"
// className="button--header-close"
icon={ICONS.REMOVE}
{...closeButtonNavigationProps}
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
</Tooltip>
</div>
)
)}
<Button
onClick={openMobileNavigation}
icon={ICONS.MENU}
iconSize={24}
className="header__menu--mobile"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
</>
)} )}
<Button
onClick={openMobileNavigation}
icon={ICONS.MENU}
iconSize={24}
className="header__menu--mobile"
// @if TARGET='app'
onDoubleClick={e => {
e.stopPropagation();
}}
// @endif
/>
</div> </div>
</header> </header>
); );

View file

@ -19,14 +19,23 @@ type Props = {
noHeader: boolean, noHeader: boolean,
noFooter: boolean, noFooter: boolean,
noSideNavigation: boolean, noSideNavigation: boolean,
backout: { backFunction: () => void, backTitle: string },
}; };
function Page(props: Props) { function Page(props: Props) {
const { children, className, authPage = false, noHeader = false, noFooter = false, noSideNavigation = false } = props; const {
children,
className,
authPage = false,
noHeader = false,
noFooter = false,
noSideNavigation = false,
backout,
} = props;
return ( return (
<Fragment> <Fragment>
{!noHeader && <Header authHeader={authPage} />} {!noHeader && <Header authHeader={authPage} backout={backout} />}
<div className={classnames('main-wrapper__inner')}> <div className={classnames('main-wrapper__inner')}>
<main className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage })}>{children}</main> <main className={classnames(MAIN_CLASS, className, { 'main--full-width': authPage })}>{children}</main>
{!authPage && !noSideNavigation && <SideNavigation />} {!authPage && !noSideNavigation && <SideNavigation />}

View file

@ -40,6 +40,7 @@ import Welcome from 'page/welcome';
import CreatorDashboard from 'page/creatorDashboard'; import CreatorDashboard from 'page/creatorDashboard';
import RewardsVerifyPage from 'page/rewardsVerify'; import RewardsVerifyPage from 'page/rewardsVerify';
import CheckoutPage from 'page/checkoutPage'; import CheckoutPage from 'page/checkoutPage';
import ChannelNew from 'page/channelNew';
import BuyPage from 'page/buy'; import BuyPage from 'page/buy';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
@ -188,6 +189,7 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} /> <Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} />
<PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} /> <PrivateRoute {...props} path={`/$/${PAGES.INVITE}`} component={InvitePage} />
<PrivateRoute {...props} path={`/$/${PAGES.CHANNEL_NEW}`} component={ChannelNew} />
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISHED}`} component={FileListPublished} /> <PrivateRoute {...props} path={`/$/${PAGES.PUBLISHED}`} component={FileListPublished} />
<PrivateRoute {...props} path={`/$/${PAGES.CREATOR_DASHBOARD}`} component={CreatorDashboard} /> <PrivateRoute {...props} path={`/$/${PAGES.CREATOR_DASHBOARD}`} component={CreatorDashboard} />
<PrivateRoute {...props} path={`/$/${PAGES.PUBLISH}`} component={PublishPage} /> <PrivateRoute {...props} path={`/$/${PAGES.PUBLISH}`} component={PublishPage} />

View file

@ -1,128 +1,126 @@
// @flow // @flow
import React, { useState } from 'react'; import React from 'react';
import { FormField } from 'component/common/form';
import FileSelector from 'component/common/file-selector'; import FileSelector from 'component/common/file-selector';
import Button from 'component/button';
import { SPEECH_URLS } from 'lbry-redux'; import { SPEECH_URLS } from 'lbry-redux';
import uuid from 'uuid/v4'; import { FormField, Form } from 'component/common/form';
import Button from 'component/button';
import Card from 'component/common/card';
import { generateThumbnailName } from 'util/generate-thumbnail-name';
import usePersistedState from 'effects/use-persisted-state';
const accept = '.png, .jpg, .jpeg, .gif'; const accept = '.png, .jpg, .jpeg, .gif';
const SOURCE_URL = 'url';
const SOURCE_UPLOAD = 'upload';
const SPEECH_READY = 'READY'; const SPEECH_READY = 'READY';
const SPEECH_UPLOADING = 'UPLOADING'; const SPEECH_UPLOADING = 'UPLOADING';
type Props = { type Props = {
assetName: string, assetName: string,
currentValue: ?string, currentValue: ?string,
onUpdate: string => void, onUpdate: string => void,
recommended: string, recommended: string,
title: string,
onDone?: () => void,
}; };
function SelectAsset(props: Props) { function SelectAsset(props: Props) {
const { onUpdate, assetName, currentValue, recommended } = props; const { onUpdate, onDone, assetName, recommended, title } = props;
const [assetSource, setAssetSource] = useState(SOURCE_URL); const [pathSelected, setPathSelected] = React.useState('');
const [pathSelected, setPathSelected] = useState(''); const [fileSelected, setFileSelected] = React.useState<any>(null);
const [fileSelected, setFileSelected] = useState<any>(null); const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY);
const [uploadStatus, setUploadStatus] = useState(SPEECH_READY); const [useUrl, setUseUrl] = usePersistedState('thumbnail-upload:mode', false);
const [error, setError] = useState(); const [url, setUrl] = React.useState('');
const [error, setError] = React.useState();
function doUploadAsset(file) { React.useEffect(() => {
if (pathSelected && fileSelected) {
doUploadAsset();
}
}, [pathSelected, fileSelected]);
function doUploadAsset() {
const uploadError = (error = '') => { const uploadError = (error = '') => {
setError(error); setError(error);
}; };
const setUrl = path => { const onSuccess = thumbnailUrl => {
setUploadStatus(SPEECH_READY); setUploadStatus(SPEECH_READY);
onUpdate(path); onUpdate(thumbnailUrl);
setAssetSource(SOURCE_URL);
if (onDone) {
onDone();
}
}; };
setUploadStatus(SPEECH_UPLOADING); setUploadStatus(SPEECH_UPLOADING);
const data = new FormData(); const data = new FormData();
const name = uuid(); const name = generateThumbnailName();
data.append('name', name); data.append('name', name);
data.append('file', file); data.append('file', fileSelected);
return fetch(SPEECH_URLS.SPEECH_PUBLISH, { return fetch(SPEECH_URLS.SPEECH_PUBLISH, {
method: 'POST', method: 'POST',
body: data, body: data,
}) })
.then(response => response.json()) .then(response => response.json())
.then(json => (json.success ? setUrl(`${json.data.serveUrl}`) : uploadError(json.message))) .then(json => (json.success ? onSuccess(`${json.data.serveUrl}`) : uploadError(json.message)))
.catch(err => uploadError(err.message)); .catch(err => {
uploadError(err.message);
});
} }
return ( return (
<fieldset-section> <Card
<fieldset-group className="fieldset-group--smushed"> title={title || __('Choose Image')}
<FormField actions={
type="select" <Form onSubmit={onDone}>
name={assetName} {error && <div className="error__text">{error}</div>}
value={assetSource} {useUrl ? (
onChange={e => setAssetSource(e.target.value)} <FormField
label={__(assetName + ' source')} autoFocus
> type={'text'}
<option key={'lmmnop'} value={'url'}> name={'thumbnail'}
URL label={`${assetName} ${recommended}`}
</option> placeholder={'https://example.com/image.png'}
<option key={'lmmnopq'} value={'upload'}> value={url}
UPLOAD onChange={e => {
</option> setUrl(e.target.value);
</FormField> onUpdate(e.target.value);
{assetSource === SOURCE_UPLOAD && ( }}
<div> />
{error && <div className="error__text">{error}</div>} ) : (
{!pathSelected && ( <FileSelector
<FileSelector autoFocus
label={'File to upload'} disabled={uploadStatus === SPEECH_UPLOADING}
name={'assetSelector'} label={uploadStatus === SPEECH_UPLOADING ? __('Uploading...') : __('File to upload')}
onFileChosen={file => { name="assetSelector"
if (file.name) { currentPath={pathSelected}
setPathSelected(file.path || file.name); onFileChosen={file => {
setFileSelected(file); if (file.name) {
} setFileSelected(file);
}} // file.path is undefined in web but available in electron
accept={accept} setPathSelected(file.name || file.path);
/> }
)} }}
{pathSelected && ( accept={accept}
<div> />
{`...${pathSelected.slice(-18)}`} {uploadStatus}{' '} )}
<Button button={'primary'} onClick={() => doUploadAsset(fileSelected)}>
Upload <div className="section__actions">
</Button>{' '} {onDone && (
<Button <Button button="primary" type="submit" label={__('Done')} disabled={uploadStatus === SPEECH_UPLOADING} />
button={'secondary'}
onClick={() => {
setPathSelected('');
setFileSelected(null);
setError(null);
}}
>
Clear
</Button>
</div>
)} )}
<FormField
name="toggle-upload"
type="checkbox"
label={__('Use a URL')}
checked={useUrl}
onChange={() => setUseUrl(!useUrl)}
/>
</div> </div>
)} </Form>
{assetSource === SOURCE_URL && ( }
<FormField />
type={'text'}
name={'thumbnail'}
label={__(assetName + ' ' + recommended)}
placeholder={'https://example.com/image.png'}
disabled={false}
value={currentValue}
onChange={e => {
onUpdate(e.target.value);
}}
/>
)}
</fieldset-group>
</fieldset-section>
); );
} }

View file

@ -80,88 +80,82 @@ export default function YoutubeTransferStatus(props: Props) {
return ( return (
hasChannels && hasChannels &&
!isYoutubeTransferComplete && ( !isYoutubeTransferComplete && (
<div> <Card
<Card title={youtubeChannels.length > 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')}
title={youtubeChannels.length > 1 ? __('Your YouTube Channels') : __('Your YouTube Channel')} subtitle={
subtitle={ <span>
<span> {hasPendingTransfers &&
{hasPendingTransfers && __('Your videos are currently being transferred. There is nothing else for you to do.')}
__('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 && __('Your videos are ready to be transferred.')} {!transferEnabled && !hasPendingTransfers && __('Please check back later.')}
{!transferEnabled && !hasPendingTransfers && __('Please check back later.')} </span>
</span> }
} body={
body={ <section>
<section> {youtubeChannels.map((channel, index) => {
{youtubeChannels.map((channel, index) => { const { lbry_channel_name: channelName, channel_claim_id: claimId, status_token: statusToken } = channel;
const { const url = buildURI({ channelName, channelClaimId: claimId });
lbry_channel_name: channelName, const transferState = getMessage(channel);
channel_claim_id: claimId, return (
status_token: statusToken, <div key={url} className="card--inline">
} = channel; {claimId ? (
const url = buildURI({ channelName, channelClaimId: claimId }); <ClaimPreview
const transferState = getMessage(channel); uri={url}
return ( actions={<span className="help">{transferState}</span>}
<div key={url} className="card--inline"> properties={false}
{claimId ? ( />
<ClaimPreview ) : (
uri={url} <div className="section--padded">
actions={<span className="help">{transferState}</span>} <p>
properties={false} <I18nMessage
/> tokens={{
) : ( channelName,
<div className="section--padded"> }}
<p> >
<I18nMessage %channelName% is not yet ready to be transferred. Please allow up to one week, though it is
tokens={{ frequently faster.
channelName, </I18nMessage>
}} </p>
> <p className="help">
%channelName% is not yet ready to be transferred. Please allow up to one week, though it is <I18nMessage
frequently faster. tokens={{
</I18nMessage> statusLink: <Button button="link" href={STATUS_URL + statusToken} label={__('here')} />,
</p> faqLink: <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/youtube" />,
<p className="help"> }}
<I18nMessage >
tokens={{ You can check your status %statusLink%. This %faqLink% explains the program in more detail.
statusLink: <Button button="link" href={STATUS_URL + statusToken} label={__('here')} />, </I18nMessage>
faqLink: <Button button="link" label={__('FAQ')} href="https://lbry.com/faq/youtube" />, </p>
}} </div>
> )}
You can check your status %statusLink%. This %faqLink% explains the program in more detail. </div>
</I18nMessage> );
</p> })}
</div> {videosImported && (
)} <div className="section help">{__('%complete% / %total% videos transferred', { complete, total })}</div>
</div> )}
); </section>
})} }
{videosImported && ( actions={
<div className="section help">{__('%complete% / %total% videos transferred', { complete, total })}</div> transferEnabled ? (
)} <div className="card__actions">
</section> <Button
} button="primary"
actions={ disabled={youtubeImportPending}
transferEnabled ? ( onClick={claimChannels}
<div className="card__actions"> label={youtubeChannels.length > 1 ? __('Claim Channels') : __('Claim Channel')}
<Button />
button="primary" <Button button="link" label={__('Learn more')} href="https://lbry.com/faq/youtube#transfer" />
disabled={youtubeImportPending} </div>
onClick={claimChannels} ) : !hideChannelLink ? (
label={youtubeChannels.length > 1 ? __('Claim Channels') : __('Claim Channel')} <div className="card__actions">
/> <Button button="primary" navigate={`/$/${PAGES.CHANNELS}`} label={__('View Your Channels')} />
<Button button="link" label={__('Learn more')} href="https://lbry.com/faq/youtube#transfer" /> </div>
</div> ) : (
) : !hideChannelLink ? ( false
<div className="card__actions"> )
<Button button="primary" navigate={`/$/${PAGES.CHANNELS}`} label={__('View Your Channels')} /> }
</div> />
) : (
false
)
}
/>
</div>
) )
); );
} }

View file

@ -1,4 +1,5 @@
export const MINIMUM_PUBLISH_BID = 0.0001; 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_ANONYMOUS = 'anonymous';
export const CHANNEL_NEW = 'new'; export const CHANNEL_NEW = 'new';

View file

@ -105,5 +105,6 @@ export const PINNED = 'Pinned';
export const BUY = 'Buy'; export const BUY = 'Buy';
export const SEND = 'Send'; export const SEND = 'Send';
export const RECEIVE = 'Receive'; export const RECEIVE = 'Receive';
export const CAMERA = 'Camera';
export const OPEN_LOG = 'FilePlus'; export const OPEN_LOG = 'FilePlus';
export const OPEN_LOG_FOLDER = 'Folder'; export const OPEN_LOG_FOLDER = 'Folder';

View file

@ -42,3 +42,4 @@ export const SIGN_OUT = 'sign_out';
export const LIQUIDATE_SUPPORTS = 'liquidate_supports'; export const LIQUIDATE_SUPPORTS = 'liquidate_supports';
export const CONFIRM_AGE = 'confirm_age'; export const CONFIRM_AGE = 'confirm_age';
export const REMOVE_BLOCKED = 'remove_blocked'; export const REMOVE_BLOCKED = 'remove_blocked';
export const IMAGE_UPLOAD = 'image_upload';

View file

@ -39,3 +39,4 @@ exports.CREATOR_DASHBOARD = 'dashboard';
exports.CHECKOUT = 'checkout'; exports.CHECKOUT = 'checkout';
exports.CODE_2257 = '2257'; exports.CODE_2257 = '2257';
exports.BUY = 'buy'; exports.BUY = 'buy';
exports.CHANNEL_NEW = 'channelnew';

View file

@ -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);

View file

@ -0,0 +1,31 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import SelectAsset from 'component/selectAsset';
type Props = {
closeModal: () => void,
currentValue: string,
title: string,
helpText: string,
onUpdate: string => void,
assetName: string,
};
function ModalImageUpload(props: Props) {
const { closeModal, currentValue, title, assetName, helpText, onUpdate } = props;
return (
<Modal isOpen type="card" onAborted={closeModal} contentLabel={title}>
<SelectAsset
onUpdate={v => onUpdate(v)}
currentValue={currentValue}
assetName={assetName}
recommended={helpText}
onDone={closeModal}
/>
</Modal>
);
}
export default ModalImageUpload;

View file

@ -40,6 +40,7 @@ import ModalSignOut from 'modal/modalSignOut';
import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate'; import ModalSupportsLiquidate from 'modal/modalSupportsLiquidate';
import ModalConfirmAge from 'modal/modalConfirmAge'; import ModalConfirmAge from 'modal/modalConfirmAge';
import ModalFileSelection from 'modal/modalFileSelection'; import ModalFileSelection from 'modal/modalFileSelection';
import ModalImageUpload from 'modal/modalImageUpload';
type Props = { type Props = {
modal: { id: string, modalProps: {} }, modal: { id: string, modalProps: {} },
@ -143,6 +144,8 @@ function ModalRouter(props: Props) {
return <ModalSupportsLiquidate {...modalProps} />; return <ModalSupportsLiquidate {...modalProps} />;
case MODALS.REMOVE_BLOCKED: case MODALS.REMOVE_BLOCKED:
return <ModalRemoveBlocked {...modalProps} />; return <ModalRemoveBlocked {...modalProps} />;
case MODALS.IMAGE_UPLOAD:
return <ModalImageUpload {...modalProps} />;
default: default:
return null; return null;
} }

View file

@ -11,6 +11,7 @@ import {
} from 'lbry-redux'; } from 'lbry-redux';
import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc'; import { selectBlackListedOutpoints, doFetchSubCount, makeSelectSubCountForUri } from 'lbryinc';
import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions'; import { makeSelectIsSubscribed } from 'redux/selectors/subscriptions';
import { doOpenModal } from 'redux/actions/app';
import ChannelPage from './view'; import ChannelPage from './view';
const select = (state, props) => ({ const select = (state, props) => ({
@ -28,6 +29,7 @@ const select = (state, props) => ({
}); });
const perform = dispatch => ({ const perform = dispatch => ({
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
fetchSubCount: claimId => dispatch(doFetchSubCount(claimId)), fetchSubCount: claimId => dispatch(doFetchSubCount(claimId)),
}); });

View file

@ -1,6 +1,6 @@
// @flow // @flow
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { useState, useEffect } from 'react'; import React from 'react';
import { parseURI } from 'lbry-redux'; import { parseURI } from 'lbry-redux';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import Page from 'component/page'; import Page from 'component/page';
@ -8,7 +8,7 @@ import SubscribeButton from 'component/subscribeButton';
import BlockButton from 'component/blockButton'; import BlockButton from 'component/blockButton';
import ShareButton from 'component/shareButton'; import ShareButton from 'component/shareButton';
import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; 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 Button from 'component/button';
import { formatLbryUrlForWeb } from 'util/url'; import { formatLbryUrlForWeb } from 'util/url';
import ChannelContent from 'component/channelContent'; import ChannelContent from 'component/channelContent';
@ -26,6 +26,7 @@ import ClaimSupportButton from 'component/claimSupportButton';
const PAGE_VIEW_QUERY = `view`; const PAGE_VIEW_QUERY = `view`;
const ABOUT_PAGE = `about`; const ABOUT_PAGE = `about`;
const DISCUSSION_PAGE = `discussion`; const DISCUSSION_PAGE = `discussion`;
const EDIT_PAGE = 'edit';
type Props = { type Props = {
uri: string, uri: string,
@ -34,8 +35,6 @@ type Props = {
cover: ?string, cover: ?string,
thumbnail: ?string, thumbnail: ?string,
page: number, page: number,
location: { search: string },
history: { push: string => void },
match: { params: { attribute: ?string } }, match: { params: { attribute: ?string } },
channelIsMine: boolean, channelIsMine: boolean,
isSubscribed: boolean, isSubscribed: boolean,
@ -52,14 +51,11 @@ type Props = {
function ChannelPage(props: Props) { function ChannelPage(props: Props) {
const { const {
uri, uri,
claim,
title, title,
cover, cover,
history,
location,
page, page,
channelIsMine, channelIsMine,
thumbnail,
claim,
isSubscribed, isSubscribed,
channelIsBlocked, channelIsBlocked,
blackListedOutpoints, blackListedOutpoints,
@ -67,19 +63,27 @@ function ChannelPage(props: Props) {
subCount, subCount,
pending, pending,
} = props; } = props;
const {
const { channelName } = parseURI(uri); push,
const { search } = location; goBack,
location: { search },
} = useHistory();
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined; const currentView = urlParams.get(PAGE_VIEW_QUERY) || undefined;
const [coverError, setCoverError] = useState(false); 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 { 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 claimId = claim.claim_id;
const formattedSubCount = Number(subCount).toLocaleString(); 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. // 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 // We don't want to use links here because we can't animate the tab change and using links
@ -97,42 +101,60 @@ function ChannelPage(props: Props) {
} else { } else {
search += `${PAGE_VIEW_QUERY}=${DISCUSSION_PAGE}`; search += `${PAGE_VIEW_QUERY}=${DISCUSSION_PAGE}`;
} }
history.push(`${url}${search}`);
push(`${url}${search}`);
} }
function doneEditing() { function onDone() {
setEditing(false); setEditing(false);
setThumbPreview(thumbnail); goBack();
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]);
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(() => { React.useEffect(() => {
if (!channelIsMine && editing) { if (!channelIsMine && editing) {
setEditing(false); setEditing(false);
} }
}, [channelIsMine, editing]);
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);
}
});
}, [claimId]);
React.useEffect(() => {
fetchSubCount(claimId);
}, [uri, fetchSubCount, claimId]);
if (editing) {
return (
<Page
noFooter
noSideNavigation={editing}
backout={{
backFunction: onDone,
title: __('Editing @%channel%', { channel: channelName }),
simpleTitle: __('Editing'),
}}
>
<ChannelEdit uri={uri} onDone={onDone} />
</Page>
);
}
return ( return (
<Page noFooter> <Page noFooter>
@ -153,39 +175,26 @@ function ChannelPage(props: Props) {
{!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />} {!channelIsBlocked && (!channelIsBlackListed || isSubscribed) && <SubscribeButton uri={permanentUrl} />}
{!isSubscribed && <BlockButton uri={permanentUrl} />} {!isSubscribed && <BlockButton uri={permanentUrl} />}
</div> </div>
{!editing && cover && !coverError && ( {cover && (
<img <img
className={classnames('channel-cover__custom', { 'channel__image--blurred': channelIsBlocked })} className={classnames('channel-cover__custom', { 'channel__image--blurred': channelIsBlocked })}
src={cover} src={cover}
onError={() => setCoverError(true)}
/> />
)} )}
{editing && <img className="channel-cover__custom" src={coverPreview} />}
{/* component that offers select/upload */}
<div className="channel__primary-info"> <div className="channel__primary-info">
{!editing && ( <ChannelThumbnail
<ChannelThumbnail className="channel__thumbnail--channel-page"
className="channel__thumbnail--channel-page" uri={uri}
uri={uri} obscure={channelIsBlocked}
obscure={channelIsBlocked} allowGifs
allowGifs />
/>
)}
{editing && (
<ChannelThumbnail
className="channel__thumbnail--channel-page"
uri={uri}
thumbnailPreview={thumbPreview}
allowGifs
/>
)}
<h1 className="channel__title">{title || '@' + channelName}</h1> <h1 className="channel__title">{title || '@' + channelName}</h1>
<div className="channel__meta"> <div className="channel__meta">
<span> <span>
{formattedSubCount} {subCount !== 1 ? __('Followers') : __('Follower')} {formattedSubCount} {subCount !== 1 ? __('Followers') : __('Follower')}
<HelpLink href="https://lbry.com/faq/views" /> <HelpLink href="https://lbry.com/faq/views" />
</span> </span>
{channelIsMine && !editing && ( {channelIsMine && (
<> <>
{pending ? ( {pending ? (
<span>{__('Your changes will be live in a few minutes')}</span> <span>{__('Your changes will be live in a few minutes')}</span>
@ -201,15 +210,6 @@ function ChannelPage(props: Props) {
)} )}
</> </>
)} )}
{channelIsMine && editing && (
<Button
button="alt"
title={__('Cancel')}
onClick={() => doneEditing()}
icon={ICONS.REMOVE}
iconSize={18}
/>
)}
</div> </div>
</div> </div>
<div className="channel-cover__gradient" /> <div className="channel-cover__gradient" />
@ -220,22 +220,12 @@ function ChannelPage(props: Props) {
<Tab>{editing ? __('Editing Your Channel') : __('About')}</Tab> <Tab>{editing ? __('Editing Your Channel') : __('About')}</Tab>
<Tab disabled={editing}>{__('Comments')}</Tab> <Tab disabled={editing}>{__('Comments')}</Tab>
</TabList> </TabList>
<TabPanels> <TabPanels>
<TabPanel> <TabPanel>
<ChannelContent uri={uri} channelIsBlackListed={channelIsBlackListed} /> <ChannelContent uri={uri} channelIsBlackListed={channelIsBlackListed} />
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
{editing ? ( <ChannelAbout uri={uri} />
<ChannelEdit
uri={uri}
doneEditing={doneEditing}
updateThumb={v => setThumbPreview(v)}
updateCover={v => setCoverPreview(v)}
/>
) : (
<ChannelAbout uri={uri} />
)}
</TabPanel> </TabPanel>
<TabPanel> <TabPanel>
<ChannelDiscussion uri={uri} /> <ChannelDiscussion uri={uri} />
@ -246,4 +236,4 @@ function ChannelPage(props: Props) {
); );
} }
export default withRouter(ChannelPage); export default ChannelPage;

View file

@ -0,0 +1,7 @@
import { connect } from 'react-redux';
import ChannelNew from './view';
const select = () => ({});
const perform = () => ({});
export default connect(select, perform)(ChannelNew);

View file

@ -0,0 +1,25 @@
// @flow
import React from 'react';
import ChannelEdit from 'component/channelEdit';
import Page from 'component/page';
import { withRouter } from 'react-router';
import * as PAGES from 'constants/pages';
type Props = {
history: { push: string => void, goBack: () => void },
};
function ChannelNew(props: Props) {
const { history } = props;
return (
<Page
noSideNavigation
backout={{ backFunction: () => history.goBack(), title: __('Create Channel') }}
className="main--auth-page"
>
<ChannelEdit onDone={() => history.push(`/$/${PAGES.CHANNELS}`)} />
</Page>
);
}
export default withRouter(ChannelNew);

View file

@ -6,7 +6,6 @@ import {
selectFetchingMyChannels, selectFetchingMyChannels,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectYoutubeChannels } from 'redux/selectors/user'; import { selectYoutubeChannels } from 'redux/selectors/user';
import { doOpenModal } from 'redux/actions/app';
import ChannelsPage from './view'; import ChannelsPage from './view';
const select = state => ({ const select = state => ({
@ -17,7 +16,6 @@ const select = state => ({
}); });
const perform = dispatch => ({ const perform = dispatch => ({
openModal: id => dispatch(doOpenModal(id)),
fetchChannelListMine: () => dispatch(doFetchChannelListMine()), fetchChannelListMine: () => dispatch(doFetchChannelListMine()),
}); });

View file

@ -1,5 +1,4 @@
// @flow // @flow
import * as MODALS from 'constants/modal_types';
import * as ICONS from 'constants/icons'; import * as ICONS from 'constants/icons';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import ClaimList from 'component/claimList'; import ClaimList from 'component/claimList';
@ -8,6 +7,7 @@ import Button from 'component/button';
import YoutubeTransferStatus from 'component/youtubeTransferStatus'; import YoutubeTransferStatus from 'component/youtubeTransferStatus';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import Card from 'component/common/card'; import Card from 'component/common/card';
import * as PAGES from 'constants/pages';
type Props = { type Props = {
channels: Array<ChannelClaim>, channels: Array<ChannelClaim>,
@ -15,11 +15,10 @@ type Props = {
fetchChannelListMine: () => void, fetchChannelListMine: () => void,
fetchingChannels: boolean, fetchingChannels: boolean,
youtubeChannels: ?Array<any>, youtubeChannels: ?Array<any>,
openModal: string => void,
}; };
export default function ChannelsPage(props: Props) { export default function ChannelsPage(props: Props) {
const { channels, channelUrls, fetchChannelListMine, fetchingChannels, youtubeChannels, openModal } = props; const { channels, channelUrls, fetchChannelListMine, fetchingChannels, youtubeChannels } = props;
const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length); const hasYoutubeChannels = youtubeChannels && Boolean(youtubeChannels.length);
const hasPendingChannels = channels && channels.some(channel => channel.confirmations < 0); const hasPendingChannels = channels && channels.some(channel => channel.confirmations < 0);
@ -29,23 +28,28 @@ export default function ChannelsPage(props: Props) {
return ( return (
<Page> <Page>
{hasYoutubeChannels && <YoutubeTransferStatus hideChannelLink />} <div className="card-stack">
{hasYoutubeChannels && <YoutubeTransferStatus hideChannelLink />}
{channelUrls && Boolean(channelUrls.length) && (
<Card
title={__('Your Channels')}
titleActions={
<>
<Button
button="secondary"
icon={ICONS.CHANNEL}
label={__('New Channel')}
navigate={`/$/${PAGES.CHANNEL_NEW}`}
/>
</>
}
isBodyList
body={<ClaimList isCardBody loading={fetchingChannels} uris={channelUrls} />}
/>
)}
</div>
{channelUrls && Boolean(channelUrls.length) && (
<Card
title={__('Your Channels')}
titleActions={
<Button
button="secondary"
icon={ICONS.CHANNEL}
label={__('New Channel')}
onClick={() => openModal(MODALS.CREATE_CHANNEL)}
/>
}
isBodyList
body={<ClaimList isCardBody loading={fetchingChannels} uris={channelUrls} />}
/>
)}
{!(channelUrls && channelUrls.length) && ( {!(channelUrls && channelUrls.length) && (
<React.Fragment> <React.Fragment>
{!fetchingChannels ? ( {!fetchingChannels ? (
@ -54,7 +58,7 @@ export default function ChannelsPage(props: Props) {
<h2 className="section__title--large">{__('No Channels Created Yet')}</h2> <h2 className="section__title--large">{__('No Channels Created Yet')}</h2>
<div className="section__actions"> <div className="section__actions">
<Button button="primary" label={__('New Channel')} onClick={() => openModal(MODALS.CREATE_CHANNEL)} /> <Button button="primary" label={__('New Channel')} navigate={`/$/${PAGES.CHANNEL_NEW}`} />
</div> </div>
</div> </div>
</section> </section>

View file

@ -8,6 +8,7 @@ import {
makeSelectTitleForUri, makeSelectTitleForUri,
normalizeURI, normalizeURI,
makeSelectClaimIsMine, makeSelectClaimIsMine,
makeSelectClaimIsPending,
} from 'lbry-redux'; } from 'lbry-redux';
import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions'; import { makeSelectChannelInSubscriptions } from 'redux/selectors/subscriptions';
import { selectBlackListedOutpoints } from 'lbryinc'; import { selectBlackListedOutpoints } from 'lbryinc';
@ -42,6 +43,7 @@ const select = (state, props) => {
uri, uri,
title: makeSelectTitleForUri(uri)(state), title: makeSelectTitleForUri(uri)(state),
claimIsMine: makeSelectClaimIsMine(uri)(state), claimIsMine: makeSelectClaimIsMine(uri)(state),
claimIsPending: makeSelectClaimIsPending(uri)(state),
}; };
}; };

View file

@ -22,11 +22,22 @@ type Props = {
nout: number, nout: number,
}>, }>,
title: string, title: string,
claimIsMine: Boolean, claimIsMine: boolean,
claimIsPending: boolean,
}; };
function ShowPage(props: Props) { function ShowPage(props: Props) {
const { isResolvingUri, resolveUri, uri, claim, blackListedOutpoints, location, claimIsMine, isSubscribed } = props; const {
isResolvingUri,
resolveUri,
uri,
claim,
blackListedOutpoints,
location,
claimIsMine,
isSubscribed,
claimIsPending,
} = props;
const signingChannel = claim && claim.signing_channel; const signingChannel = claim && claim.signing_channel;
const canonicalUrl = claim && claim.canonical_url; const canonicalUrl = claim && claim.canonical_url;
const claimExists = claim !== null && claim !== undefined; const claimExists = claim !== null && claim !== undefined;
@ -45,11 +56,11 @@ function ShowPage(props: Props) {
if ( if (
(resolveUri && !isResolvingUri && uri && haventFetchedYet) || (resolveUri && !isResolvingUri && uri && haventFetchedYet) ||
(claimExists && (!canonicalUrl || isMine === undefined)) (claimExists && !claimIsPending && (!canonicalUrl || isMine === undefined))
) { ) {
resolveUri(uri); resolveUri(uri);
} }
}, [resolveUri, isResolvingUri, canonicalUrl, uri, claimExists, haventFetchedYet, history, isMine]); }, [resolveUri, isResolvingUri, canonicalUrl, uri, claimExists, haventFetchedYet, history, isMine, claimIsPending]);
// Don't navigate directly to repost urls // Don't navigate directly to repost urls
// Always redirect to the actual content // Always redirect to the actual content

View file

@ -53,6 +53,11 @@
align-items: center; align-items: center;
} }
.card--after-tabs {
@extend .card;
margin-top: var(--spacing-l);
}
.card__actions { .card__actions {
display: flex; display: flex;
align-items: center; align-items: center;
@ -107,6 +112,8 @@
.card__actions--between { .card__actions--between {
@include between; @include between;
align-items: center;
width: 100%;
} }
.card__actions--center { .card__actions--center {
@ -161,15 +168,21 @@
align-items: center; align-items: center;
font-size: var(--font-title); font-size: var(--font-title);
font-weight: var(--font-weight-light); font-weight: var(--font-weight-light);
& > *:not(:last-child) { & > *:not(:last-child) {
margin-right: var(--spacing-m); margin-right: var(--spacing-m);
} }
/* .badge rule inherited from file page prices, should be refactored */ /* .badge rule inherited from file page prices, should be refactored */
.badge { .badge {
float: right; float: right;
margin-left: var(--spacing-s); margin-left: var(--spacing-s);
margin-top: 8px; // should be flex'd, but don't blame me! I just moved it down 3px margin-top: 8px; // should be flex'd, but don't blame me! I just moved it down 3px
} }
@media (max-width: $breakpoint-small) {
font-size: var(--font-body);
}
} }
.card__title-actions { .card__title-actions {

View file

@ -20,9 +20,12 @@ $metadata-z-index: 1;
.channel-cover, .channel-cover,
.channel-cover__custom { .channel-cover__custom {
min-height: var(--cover-photo-height); min-height: var(--cover-photo-height);
height: 100%;
width: 100%; width: 100%;
border-top-left-radius: var(--card-radius); border-top-left-radius: var(--card-radius);
border-top-right-radius: var(--card-radius); border-top-right-radius: var(--card-radius);
border: 1px sold var(--color-border);
border-bottom: none;
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
// Yikes // Yikes
@ -72,7 +75,8 @@ $metadata-z-index: 1;
box-shadow: var(--card-box-shadow); box-shadow: var(--card-box-shadow);
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
display: none; top: 0;
margin-top: var(--spacing-m);
} }
} }
@ -119,7 +123,7 @@ $metadata-z-index: 1;
z-index: $metadata-z-index; z-index: $metadata-z-index;
// Jump over the thumbnail photo because it is absolutely positioned // Jump over the thumbnail photo because it is absolutely positioned
// Then add normal page spacing, _then_ add the actual padding // Then add normal page spacing, _then_ add the actual padding
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-l)); padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-xl));
padding-right: var(--spacing-m); padding-right: var(--spacing-m);
padding-bottom: var(--spacing-m); padding-bottom: var(--spacing-m);
margin-top: var(--spacing-xl); margin-top: var(--spacing-xl);
@ -128,7 +132,7 @@ $metadata-z-index: 1;
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
padding-left: var(--spacing-m); padding-left: var(--spacing-m);
margin-top: 0; margin-top: calc(var(--channel-thumbnail-width) + var(--spacing-l));
} }
} }
@ -168,7 +172,6 @@ $metadata-z-index: 1;
.channel__quick-actions { .channel__quick-actions {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
margin-top: var(--spacing-s);
margin-left: var(--spacing-m); margin-left: var(--spacing-m);
position: absolute; position: absolute;
top: 0; top: 0;
@ -187,8 +190,41 @@ $metadata-z-index: 1;
} }
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
flex-direction: column;
> * { > * {
margin-right: 0;
margin-bottom: var(--spacing-xs); margin-bottom: var(--spacing-xs);
margin-left: auto;
// Needed for specificity above
&:not(:last-child) {
margin-right: 0;
}
}
}
}
.channel__edit-thumb {
position: absolute;
top: 0;
left: var(--spacing-l);
margin-top: calc(var(--spacing-m) * 7);
z-index: $metadata-z-index;
> * {
padding: 0 var(--spacing-xs);
&:not(:last-child) {
margin-right: var(--spacing-m);
}
}
@media (max-width: $breakpoint-small) {
margin-top: 0;
.button {
margin-top: var(--spacing-l);
} }
} }
} }

View file

@ -87,7 +87,7 @@ fieldset-group {
border-color: var(--color-input-border); border-color: var(--color-input-border);
color: var(--color-text-help); color: var(--color-text-help);
background-color: var(--color-input-bg); background-color: var(--color-input-bg);
border-right: 1px solid var(--color-text-help); border-right: 1px solid var(--border-color);
} }
} }
@ -133,8 +133,6 @@ fieldset-group {
.form-field__help { .form-field__help {
@extend .help; @extend .help;
margin-top: var(--spacing-xxs);
margin-bottom: var(--spacing-s);
} }
.form-field__help + .checkbox, .form-field__help + .checkbox,

View file

@ -1,5 +1,7 @@
.main-wrapper { .main-wrapper {
position: relative; position: relative;
margin-left: auto;
margin-right: auto;
} }
.main-wrapper--mac { .main-wrapper--mac {
@ -25,6 +27,8 @@
.main { .main {
position: relative; position: relative;
width: calc(100% - var(--side-nav-width) - var(--spacing-l)); width: calc(100% - var(--side-nav-width) - var(--spacing-l));
margin-right: auto;
margin-left: auto;
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
width: 100%; width: 100%;

View file

@ -109,7 +109,9 @@
flex-wrap: wrap; flex-wrap: wrap;
> * { > * {
margin-bottom: var(--spacing-s); &:not(:only-of-type) {
margin-bottom: var(--spacing-s);
}
} }
} }

View file

@ -15,12 +15,14 @@
} }
.tabs__list--channel-page { .tabs__list--channel-page {
padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-l)); padding-left: calc(var(--channel-thumbnail-width) + var(--spacing-xl));
padding-right: var(--spacing-m); padding-right: var(--spacing-m);
margin-bottom: var(--spacing-l); margin-bottom: var(--spacing-l);
height: 4rem; height: 4rem;
border-bottom-left-radius: var(--card-radius); border-bottom-left-radius: var(--card-radius);
border-bottom-right-radius: var(--card-radius); border-bottom-right-radius: var(--card-radius);
border: 1px solid var(--color-border);
border-top: none;
@media (max-width: $breakpoint-small) { @media (max-width: $breakpoint-small) {
padding-left: var(--spacing-m); padding-left: var(--spacing-m);
@ -46,6 +48,10 @@
&:focus { &:focus {
box-shadow: none; box-shadow: none;
} }
@media (max-width: $breakpoint-xsmall) {
margin-right: var(--spacing-m);
}
} }
.tab__divider { .tab__divider {

View file

@ -0,0 +1,7 @@
// @flow
export const generateThumbnailName = (): string => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
return text;
};

View file

@ -957,10 +957,10 @@
prop-types "^15.6.2" prop-types "^15.6.2"
scheduler "^0.18.0" scheduler "^0.18.0"
"@lbry/components@^4.2.2": "@lbry/components@^4.2.5":
version "4.2.2" version "4.2.5"
resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.2.2.tgz#023b8224e180b69cd8b5d77242441742bca26d0f" resolved "https://registry.yarnpkg.com/@lbry/components/-/components-4.2.5.tgz#21d2cad296c015c6300727e706a1456e72aae600"
integrity sha512-CziwuALDiv/DXT5zwkVK8cfF914WyOqKg8GkqIk/f9vc7VXZx9OlRfBadnpGDrezrjjHn7onwOreQrwzB5PcNA== integrity sha512-UmE4nkvTQnZX4obOiR650apXarRmvXW+Ieezu/M4LZNKL/CRmHJIRrqcSMtPugDf/Ijukx/4oqrq2M8Ww0sgyw==
"@mapbox/hast-util-table-cell-style@^0.1.3": "@mapbox/hast-util-table-cell-style@^0.1.3":
version "0.1.3" version "0.1.3"
@ -6347,9 +6347,9 @@ lazy-val@^1.0.4:
yargs "^13.2.2" yargs "^13.2.2"
zstd-codec "^0.1.1" zstd-codec "^0.1.1"
lbry-redux@lbryio/lbry-redux#eb47b7e5b6cc24db93b2b66cf1153b02858caf58: lbry-redux@lbryio/lbry-redux#906199d866a187015668a27363f010828c15979a:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/eb47b7e5b6cc24db93b2b66cf1153b02858caf58" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/906199d866a187015668a27363f010828c15979a"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"