disable file input while awaiting publish
add spinner to publishes in sidebar
add spinner and Publishing title on Publish page
add WebUploadList to Publishes
add WebUploadItem
 - thumb
 - name
 - progress bar
 - abort button
beforeunload prevent closing tab / navigation
enforce and notify about publish size limit
6 outstanding flow complaints
This commit is contained in:
jessop 2019-10-10 20:37:18 -04:00 committed by Sean Yesmunt
parent dc1863890e
commit 3cc69ddaf0
22 changed files with 260 additions and 28 deletions

View file

@ -1,4 +1,6 @@
declare type WebFile = {
name: string,
title?: string,
path?: string,
size?: string,
}

View file

@ -1,8 +1,8 @@
import { HEADERS, Lbry } from 'lbry-redux';
import { Lbry } from 'lbry-redux';
import apiPublishCallViaWeb from './publish';
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
export const SDK_API_URL = process.env.SDK_API_URL || 'https://api.lbry.tv/api/v1/proxy';
Lbry.setDaemonConnectionString(SDK_API_URL);
Lbry.setOverride(
@ -11,8 +11,8 @@ Lbry.setOverride(
new Promise((resolve, reject) => {
apiPublishCallViaWeb(
SDK_API_URL,
Lbry.getApiRequestHeaders() && Object.keys(Lbry.getApiRequestHeaders()).includes(HEADERS.AUTH_TOKEN)
? Lbry.getApiRequestHeaders()[HEADERS.AUTH_TOKEN]
Lbry.getApiRequestHeaders() && Object.keys(Lbry.getApiRequestHeaders()).includes(X_LBRY_AUTH_TOKEN)
? Lbry.getApiRequestHeaders()[X_LBRY_AUTH_TOKEN]
: '',
'publish',
params,

View file

@ -1,5 +1,13 @@
// @flow
import { HEADERS } from 'lbry-redux';
/*
https://api.lbry.tv/api/v1/proxy currently expects publish to consist
of a multipart/form-data POST request with:
- 'file' binary
- 'json_payload' collection of publish params to be passed to the server's sdk.
*/
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
import { doUpdateUploadProgress } from 'lbryinc';
// A modified version of Lbry.apiCall that allows
// to perform calling methods at arbitrary urls
// and pass form file fields
@ -26,27 +34,34 @@ export default function apiPublishCallViaWeb(
body.append('file', fileField);
body.append('json_payload', jsonPayload);
function makeRequest(connectionString, method, token, body) {
function makeRequest(connectionString, method, token, body, params) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.open(method, connectionString);
xhr.setRequestHeader(HEADERS.AUTH_TOKEN, token);
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
xhr.responseType = 'json';
xhr.upload.onprogress = e => {
let percentComplete = Math.ceil((e.loaded / e.total) * 100);
console.log(percentComplete); // put your upload state update here
window.store.dispatch(doUpdateUploadProgress(percentComplete, params, xhr));
};
xhr.onload = () => {
window.store.dispatch(doUpdateUploadProgress(undefined, params));
resolve(xhr);
};
xhr.onerror = () => {
reject({ status: xhr.status, statusText: xhr.statusText });
window.store.dispatch(doUpdateUploadProgress(undefined, params));
reject(new Error(__('There was a problem with your upload')));
};
xhr.onabort = () => {
window.store.dispatch(doUpdateUploadProgress(undefined, params));
reject(new Error(__('You aborted your publish upload')));
};
xhr.send(body);
});
}
return makeRequest(connectionString, 'POST', token, body)
return makeRequest(connectionString, 'POST', token, body, params)
.then(xhr => {
let error;
if (xhr) {
@ -55,7 +70,7 @@ export default function apiPublishCallViaWeb(
} else if (xhr.statusText) {
error = new Error(xhr.statusText);
} else {
error = new Error('Upload likely timed out. Try a smaller file while we work on this.');
error = new Error(__('Upload likely timed out. Try a smaller file while we work on this.'));
}
}

View file

@ -8,6 +8,7 @@ import {
doFetchAccessToken,
selectAccessToken,
selectGetSyncErrorMessage,
selectUploadCount,
} from 'lbryinc';
import { doFetchTransactions, doFetchChannelListMine, selectBalance } from 'lbry-redux';
import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings';
@ -26,6 +27,7 @@ const select = state => ({
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
syncError: selectGetSyncErrorMessage(state),
accessToken: selectAccessToken(state),
uploadCount: selectUploadCount(state),
});
const perform = dispatch => ({

View file

@ -47,6 +47,7 @@ type Props = {
checkSync: () => void,
setSyncEnabled: boolean => void,
syncEnabled: boolean,
uploadCount: number,
balance: ?number,
accessToken: ?string,
syncError: ?string,
@ -68,6 +69,7 @@ function App(props: Props) {
setSyncEnabled,
syncEnabled,
checkSync,
uploadCount,
balance,
accessToken,
history,
@ -128,6 +130,16 @@ function App(props: Props) {
});
}, [balance, accessToken, hasDeterminedIfNewUser, setHasDeterminedIfNewUser]);
useEffect(() => {
if (!uploadCount) return;
const handleBeforeUnload = event => {
event.preventDefault();
event.returnValue = 'magic';
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
}, [uploadCount]);
useEffect(() => {
ReactModal.setAppElement(appRef.current);
fetchAccessToken();

View file

@ -13,6 +13,8 @@ type Props = {
fileLabel?: string,
directoryLabel?: string,
accept?: string,
error?: string,
disabled?: boolean,
};
class FileSelector extends React.PureComponent<Props> {
@ -48,7 +50,7 @@ class FileSelector extends React.PureComponent<Props> {
input: ?HTMLInputElement;
render() {
const { type, currentPath, label, fileLabel, directoryLabel, placeholder, accept } = this.props;
const { type, currentPath, label, fileLabel, directoryLabel, placeholder, accept, error, disabled } = this.props;
const buttonLabel = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory');
const placeHolder = currentPath || placeholder;
@ -58,10 +60,14 @@ class FileSelector extends React.PureComponent<Props> {
label={label}
webkitdirectory="true"
className="form-field--copyable"
error={error}
disabled={disabled}
type="text"
readOnly="readonly"
value={placeHolder || __('Choose a file')}
inputButton={<Button button="primary" onClick={this.fileInputButton} label={buttonLabel} />}
inputButton={
<Button button="primary" disabled={disabled} onClick={this.fileInputButton} label={buttonLabel} />
}
/>
<input
type={'file'}

View file

@ -8,6 +8,7 @@ const select = state => ({
filePath: makeSelectPublishFormValue('filePath')(state),
isStillEditing: selectIsStillEditing(state),
balance: selectBalance(state),
publishing: makeSelectPublishFormValue('publishing')(state),
});
const perform = dispatch => ({

View file

@ -1,10 +1,11 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import React, { useState } from 'react';
import { regexInvalidURI } from 'lbry-redux';
import FileSelector from 'component/common/file-selector';
import Button from 'component/button';
import Card from 'component/common/card';
import Spinner from 'component/spinner';
type Props = {
name: ?string,
@ -13,10 +14,14 @@ type Props = {
balance: number,
updatePublishForm: ({}) => void,
disabled: boolean,
publishing: boolean,
};
function PublishFile(props: Props) {
const { name, balance, filePath, isStillEditing, updatePublishForm, disabled } = props;
const { name, balance, filePath, isStillEditing, updatePublishForm, disabled, publishing } = props;
// This is basically for displaying the 500mb limit
const [fileError, setFileError] = useState('');
let currentFile = '';
if (filePath) {
@ -30,29 +35,57 @@ function PublishFile(props: Props) {
function handleFileChange(file: WebFile) {
// if electron, we'll set filePath to the path string because SDK is handling publishing.
// if web, we set the filePath (dumb name) to the File() object
// file.path will be undefined from web due to browser security, so it will default to the File Object.
// File.path will be undefined from web due to browser security, so it will default to the File Object.
// @if TARGET='web'
// we only need to enforce file sizes on 'web'
const PUBLISH_SIZE_LIMIT: number = 512000000;
if (typeof file !== 'string') {
if (file && file.size && Number(file.size) > PUBLISH_SIZE_LIMIT) {
setFileError('File uploads currently limited to 500MB. Download the app for unlimited publishing.');
updatePublishForm({ filePath: '', name: '' });
return;
} else {
setFileError('');
}
}
// @endif
const publishFormParams: { filePath: string | WebFile, name?: string } = {
filePath: file.path || file,
name: file.name,
};
const parsedFileName = file.name.replace(regexInvalidURI, '');
publishFormParams.name = parsedFileName.replace(' ', '-');
updatePublishForm(publishFormParams);
}
let title;
if (publishing) {
title = (
<span>
{__('Publishing')}
<Spinner type={'small'} />
</span>
);
} else {
title = isStillEditing ? __('Edit') : __('Publish');
}
return (
<Card
icon={ICONS.PUBLISH}
disabled={disabled || balance === 0}
title={isStillEditing ? __('Edit') : __('Publish')}
title={title}
subtitle={
isStillEditing ? __('You are currently editing a claim.') : __('Publish something totally wacky and wild.')
}
actions={
<React.Fragment>
<FileSelector currentPath={currentFile} onFileChosen={handleFileChange} />
<FileSelector
disabled={disabled}
currentPath={currentFile}
onFileChosen={handleFileChange}
error={fileError}
/>
{!isStillEditing && (
<p className="help">
{__('For video content, use MP4s in H264/AAC format for best compatibility.')}{' '}

View file

@ -1,4 +1,12 @@
// @flow
/*
On submit, this component calls publish, which dispatches doPublishDesktop.
doPublishDesktop calls lbry-redux Lbry publish method using lbry-redux publish state as params.
Publish simply instructs the SDK to find the file path on disk and publish it with the provided metadata.
On web, the Lbry publish method call is overridden in platform/web/api-setup, using a function in platform/web/publish.
File upload is carried out in the background by that function.
*/
import React, { useEffect, Fragment } from 'react';
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
@ -122,7 +130,7 @@ function PublishForm(props: Props) {
return (
<Fragment>
<PublishFile disabled={formDisabled}/>
<PublishFile disabled={publishing} />
<div className={classnames({ 'card--disabled': formDisabled })}>
<PublishText disabled={formDisabled} />
<Card actions={<SelectThumbnail />} />

View file

@ -24,7 +24,7 @@ function SelectAsset(props: Props) {
const { onUpdate, assetName, currentValue, recommended } = props;
const [assetSource, setAssetSource] = useState(SOURCE_URL);
const [pathSelected, setPathSelected] = useState('');
const [fileSelected, setFileSelected] = useState(null);
const [fileSelected, setFileSelected] = useState('');
const [uploadStatus, setUploadStatus] = useState(SPEECH_READY);
function doUploadAsset(file) {
@ -95,7 +95,7 @@ function SelectAsset(props: Props) {
button={'secondary'}
onClick={() => {
setPathSelected('');
setFileSelected(null);
setFileSelected('');
}}
>
Clear

View file

@ -2,7 +2,7 @@ import * as SETTINGS from 'constants/settings';
import { connect } from 'react-redux';
import { selectSubscriptions } from 'redux/selectors/subscriptions';
import { selectFollowedTags } from 'lbry-redux';
import { selectUserEmail } from 'lbryinc';
import { selectUserEmail, selectUploadCount } from 'lbryinc';
import SideBar from './view';
import { makeSelectClientSetting } from 'redux/selectors/settings';
@ -11,6 +11,7 @@ const select = state => ({
followedTags: selectFollowedTags(state),
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
email: selectUserEmail(state),
uploadCount: selectUploadCount(state),
});
const perform = () => ({});

View file

@ -6,16 +6,18 @@ import Button from 'component/button';
import Tag from 'component/tag';
import StickyBox from 'react-sticky-box/dist/esnext';
import 'css-doodle';
import Spinner from 'component/spinner';
type Props = {
subscriptions: Array<Subscription>,
followedTags: Array<Tag>,
email: ?string,
obscureSideBar: boolean,
uploadCount: number,
};
function SideBar(props: Props) {
const { subscriptions, followedTags, obscureSideBar } = props;
const { subscriptions, followedTags, obscureSideBar, uploadCount } = props;
function buildLink(path, label, icon, guide) {
return {
navigate: path ? `$/${path}` : '/',
@ -52,7 +54,18 @@ function SideBar(props: Props) {
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
},
{
...buildLink(PAGES.PUBLISHED, __('Publishes'), ICONS.PUBLISH),
...buildLink(
PAGES.PUBLISHED,
uploadCount ? (
<span>
{__('Publishes')}
<Spinner type="small" />
</span>
) : (
__('Publishes')
),
ICONS.PUBLISH
),
},
].map(linkProps => (
<li key={linkProps.label}>

View file

@ -0,0 +1,13 @@
import { connect } from 'react-redux';
import { selectCurrentUploads, selectUploadCount } from 'lbryinc';
import WebUploadList from './view';
const select = state => ({
currentUploads: selectCurrentUploads(state),
uploadCount: selectUploadCount(state),
});
export default connect(
select,
null
)(WebUploadList);

View file

@ -0,0 +1,41 @@
// @flow
import React from 'react';
import Button from 'component/button';
import CardMedia from 'component/cardMedia';
type Props = {
params: UpdatePublishFormData,
progress: string,
xhr?: () => void,
};
export default function WebUploadItem(props: Props) {
const { params, progress, xhr } = props;
return (
<li className={'claim-preview'}>
<CardMedia thumbnail={params.thumbnail_url} />
<div className={'claim-preview-metadata'}>
<div className="claim-preview-info">
<div className="claim-preview-title">{params.title}</div>
{xhr && (
<div className="card__actions--inline">
<Button
button={'primary'}
onClick={() => {
xhr.abort();
}}
label={'abort'}
/>
</div>
)}
</div>
<h2>{params.name}</h2>
<div className={'claim-upload__progress--outer'}>
<div className={'claim-upload__progress--inner'} style={{ width: `${progress}%` }}>
Uploading...
</div>
</div>
</div>
</li>
);
}

View file

@ -0,0 +1,37 @@
// @flow
import * as React from 'react';
import Card from 'component/common/card';
import WebUploadItem from './internal/web-upload-item';
export type UploadItem = {
progess: string,
params: UpdatePublishFormData,
xhr?: { abort: () => void },
};
type Props = {
currentUploads: { [key: string]: UploadItem },
uploadCount: ?number,
};
export default function WebUploadList(props: Props) {
const { currentUploads, uploadCount } = props;
return (
!!uploadCount && (
<div>
<Card
title={__('Currently Uploading')}
subtitle={<span>{__('You are currently uploading one or more files for publish.')}</span>}
body={
<section>
{Object.values(currentUploads).map(({ progress, params, xhr }) => (
<WebUploadItem key={`upload${params.name}`} progress={progress} params={params} xhr={xhr} />
))}
</section>
}
/>
</div>
)
);
}

View file

@ -0,0 +1 @@
export const X_LBRY_AUTH_TOKEN = 'X-Lbry-Auth-Token';

View file

@ -27,6 +27,12 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
navigate('/$/published');
closeModal();
}}
confirmButtonLabel={'Show me!'}
abortButtonLabel={'Thanks!'}
onAborted={() => {
clearPublish();
closeModal();
}}
>
<p>{__(`Your ${publishMessage} published to LBRY at the address`)}</p>
<blockquote>{uri}</blockquote>

View file

@ -5,6 +5,7 @@ import ClaimList from 'component/claimList';
import Page from 'component/page';
import Paginate from 'component/common/paginate';
import { PAGE_SIZE } from 'constants/claim';
import WebUploadList from 'component/webUploadList';
type Props = {
checkPendingPublishes: () => void,
@ -25,6 +26,7 @@ function FileListPublished(props: Props) {
return (
<Page notContained>
<WebUploadList />
{urls && urls.length ? (
<div className="card">
<ClaimList

View file

@ -21,6 +21,7 @@ import {
homepageReducer,
statsReducer,
syncReducer,
lbrytvReducer,
} from 'lbryinc';
import appReducer from 'redux/reducers/app';
import availabilityReducer from 'redux/reducers/availability';
@ -54,4 +55,5 @@ export default history =>
user: userReducer,
wallet: walletReducer,
sync: syncReducer,
lbrytv: lbrytvReducer,
});

View file

@ -7,6 +7,8 @@ import path from 'path';
import * as ACTIONS from 'constants/action_types';
import * as MODALS from 'constants/modal_types';
import * as PAGES from 'constants/pages';
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
import * as SETTINGS from 'constants/settings';
import {
Lbry,
doBalanceSubscribe,
@ -437,8 +439,9 @@ export function doAnalyticsView(uri, timeToStart) {
export function doSignIn() {
return (dispatch, getState) => {
// @if TARGET='web'
const authToken = getAuthToken();
Lbry.setApiHeader(HEADERS.AUTH_TOKEN, authToken);
const { auth_token: authToken } = cookie.parse(document.cookie);
Lbry.setApiHeader(X_LBRY_AUTH_TOKEN, authToken);
dispatch(doBalanceSubscribe());
dispatch(doFetchChannelListMine());
// @endif

View file

@ -198,3 +198,36 @@ $border-color--dark: var(--dm-color-04);
flex: 1;
font-size: var(--font-subtext);
}
.claim-upload {
flex: 1;
display: flex;
position: relative;
overflow: visible;
padding: var(--spacing-medium);
.media__thumb {
width: var(--file-list-thumbnail-width);
flex-shrink: 0;
margin-right: var(--spacing-medium);
}
[data-mode='dark'] & {
color: $lbry-white;
border-color: $border-color--dark;
}
}
.claim-upload__progress--outer {
width: 100%;
}
.claim-upload__progress--inner {
background: $lbry-teal-1;
color: $lbry-gray-1;
[data-mode='dark'] & {
background: $lbry-teal-4;
color: $lbry-white;
}
}

View file

@ -728,6 +728,7 @@
"Your LBRY credits are controllable by you and only you, via a wallet file stored locally on your computer.": "Your LBRY credits are controllable by you and only you, via a wallet file stored locally on your computer.",
"However, it is easy to back up manually. To backup your wallet, make a copy of the folder listed below:": "However, it is easy to back up manually. To backup your wallet, make a copy of the folder listed below:",
"Access to these files are equivalent to having access to your credits. Keep any copies you make of your wallet in a secure place. For more details on backing up and best practices %helpLink%.": "Access to these files are equivalent to having access to your credits. Keep any copies you make of your wallet in a secure place. For more details on backing up and best practices %helpLink%.",
}
"Your Channels": "Your Channels",
"Add Tags": "Add Tags",
"Available Balance": "Available Balance",