Merge pull request #4445 from lbryio/feat-newChannelCreate
new channel creating and editing
This commit is contained in:
commit
70c6034662
38 changed files with 1112 additions and 675 deletions
|
@ -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",
|
||||||
|
|
|
@ -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."
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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: '',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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 />}
|
||||||
|
|
|
@ -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} />
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
11
ui/modal/modalImageUpload/index.js
Normal file
11
ui/modal/modalImageUpload/index.js
Normal 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);
|
31
ui/modal/modalImageUpload/view.jsx
Normal file
31
ui/modal/modalImageUpload/view.jsx
Normal 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;
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
7
ui/page/channelNew/index.js
Normal file
7
ui/page/channelNew/index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import ChannelNew from './view';
|
||||||
|
|
||||||
|
const select = () => ({});
|
||||||
|
const perform = () => ({});
|
||||||
|
|
||||||
|
export default connect(select, perform)(ChannelNew);
|
25
ui/page/channelNew/view.jsx
Normal file
25
ui/page/channelNew/view.jsx
Normal 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);
|
|
@ -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()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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%;
|
||||||
|
|
|
@ -109,7 +109,9 @@
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
margin-bottom: var(--spacing-s);
|
&:not(:only-of-type) {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
7
ui/util/generate-thumbnail-name.js
Normal file
7
ui/util/generate-thumbnail-name.js
Normal 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;
|
||||||
|
};
|
12
yarn.lock
12
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Add table
Reference in a new issue