From 3cc69ddaf091c0761a91c1acad146bb87f5e49d4 Mon Sep 17 00:00:00 2001 From: jessop Date: Thu, 10 Oct 2019 20:37:18 -0400 Subject: [PATCH] UI/UX 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 --- flow-typed/web-file.js | 2 + src/platforms/web/api-setup.js | 8 ++-- src/platforms/web/publish.js | 29 +++++++++--- src/ui/component/app/index.js | 2 + src/ui/component/app/view.jsx | 12 +++++ src/ui/component/common/file-selector.jsx | 10 +++- src/ui/component/publishFile/index.js | 1 + src/ui/component/publishFile/view.jsx | 47 ++++++++++++++++--- src/ui/component/publishForm/view.jsx | 10 +++- src/ui/component/selectAsset/view.jsx | 4 +- src/ui/component/sideBar/index.js | 3 +- src/ui/component/sideBar/view.jsx | 17 ++++++- src/ui/component/webUploadList/index.js | 13 +++++ .../internal/web-upload-item.jsx | 41 ++++++++++++++++ src/ui/component/webUploadList/view.jsx | 37 +++++++++++++++ src/ui/constants/token.js | 1 + src/ui/modal/modalPublish/view.jsx | 6 +++ src/ui/page/fileListPublished/view.jsx | 2 + src/ui/reducers.js | 2 + src/ui/redux/actions/app.js | 7 ++- src/ui/scss/component/_claim-list.scss | 33 +++++++++++++ static/app-strings.json | 1 + 22 files changed, 260 insertions(+), 28 deletions(-) create mode 100644 src/ui/component/webUploadList/index.js create mode 100644 src/ui/component/webUploadList/internal/web-upload-item.jsx create mode 100644 src/ui/component/webUploadList/view.jsx create mode 100644 src/ui/constants/token.js diff --git a/flow-typed/web-file.js b/flow-typed/web-file.js index b79675071..6bb0a4d10 100644 --- a/flow-typed/web-file.js +++ b/flow-typed/web-file.js @@ -1,4 +1,6 @@ declare type WebFile = { name: string, + title?: string, path?: string, + size?: string, } diff --git a/src/platforms/web/api-setup.js b/src/platforms/web/api-setup.js index da24e2035..530a4351f 100644 --- a/src/platforms/web/api-setup.js +++ b/src/platforms/web/api-setup.js @@ -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, diff --git a/src/platforms/web/publish.js b/src/platforms/web/publish.js index 528e45608..dd50946e4 100644 --- a/src/platforms/web/publish.js +++ b/src/platforms/web/publish.js @@ -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.')); } } diff --git a/src/ui/component/app/index.js b/src/ui/component/app/index.js index dc4c37d1f..950513b88 100644 --- a/src/ui/component/app/index.js +++ b/src/ui/component/app/index.js @@ -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 => ({ diff --git a/src/ui/component/app/view.jsx b/src/ui/component/app/view.jsx index 80fd74303..965112736 100644 --- a/src/ui/component/app/view.jsx +++ b/src/ui/component/app/view.jsx @@ -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(); diff --git a/src/ui/component/common/file-selector.jsx b/src/ui/component/common/file-selector.jsx index d3489b5c6..b7e573603 100644 --- a/src/ui/component/common/file-selector.jsx +++ b/src/ui/component/common/file-selector.jsx @@ -13,6 +13,8 @@ type Props = { fileLabel?: string, directoryLabel?: string, accept?: string, + error?: string, + disabled?: boolean, }; class FileSelector extends React.PureComponent { @@ -48,7 +50,7 @@ class FileSelector extends React.PureComponent { 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 { label={label} webkitdirectory="true" className="form-field--copyable" + error={error} + disabled={disabled} type="text" readOnly="readonly" value={placeHolder || __('Choose a file')} - inputButton={