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
This commit is contained in:
parent
dc1863890e
commit
3cc69ddaf0
22 changed files with 260 additions and 28 deletions
2
flow-typed/web-file.js
vendored
2
flow-typed/web-file.js
vendored
|
@ -1,4 +1,6 @@
|
||||||
declare type WebFile = {
|
declare type WebFile = {
|
||||||
name: string,
|
name: string,
|
||||||
|
title?: string,
|
||||||
path?: string,
|
path?: string,
|
||||||
|
size?: string,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { HEADERS, Lbry } from 'lbry-redux';
|
import { Lbry } from 'lbry-redux';
|
||||||
import apiPublishCallViaWeb from './publish';
|
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';
|
export const SDK_API_URL = process.env.SDK_API_URL || 'https://api.lbry.tv/api/v1/proxy';
|
||||||
|
|
||||||
Lbry.setDaemonConnectionString(SDK_API_URL);
|
Lbry.setDaemonConnectionString(SDK_API_URL);
|
||||||
|
|
||||||
Lbry.setOverride(
|
Lbry.setOverride(
|
||||||
|
@ -11,8 +11,8 @@ Lbry.setOverride(
|
||||||
new Promise((resolve, reject) => {
|
new Promise((resolve, reject) => {
|
||||||
apiPublishCallViaWeb(
|
apiPublishCallViaWeb(
|
||||||
SDK_API_URL,
|
SDK_API_URL,
|
||||||
Lbry.getApiRequestHeaders() && Object.keys(Lbry.getApiRequestHeaders()).includes(HEADERS.AUTH_TOKEN)
|
Lbry.getApiRequestHeaders() && Object.keys(Lbry.getApiRequestHeaders()).includes(X_LBRY_AUTH_TOKEN)
|
||||||
? Lbry.getApiRequestHeaders()[HEADERS.AUTH_TOKEN]
|
? Lbry.getApiRequestHeaders()[X_LBRY_AUTH_TOKEN]
|
||||||
: '',
|
: '',
|
||||||
'publish',
|
'publish',
|
||||||
params,
|
params,
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
// @flow
|
// @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
|
// A modified version of Lbry.apiCall that allows
|
||||||
// to perform calling methods at arbitrary urls
|
// to perform calling methods at arbitrary urls
|
||||||
// and pass form file fields
|
// and pass form file fields
|
||||||
|
@ -26,27 +34,34 @@ export default function apiPublishCallViaWeb(
|
||||||
body.append('file', fileField);
|
body.append('file', fileField);
|
||||||
body.append('json_payload', jsonPayload);
|
body.append('json_payload', jsonPayload);
|
||||||
|
|
||||||
function makeRequest(connectionString, method, token, body) {
|
function makeRequest(connectionString, method, token, body, params) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let xhr = new XMLHttpRequest();
|
let xhr = new XMLHttpRequest();
|
||||||
xhr.open(method, connectionString);
|
xhr.open(method, connectionString);
|
||||||
xhr.setRequestHeader(HEADERS.AUTH_TOKEN, token);
|
xhr.setRequestHeader(X_LBRY_AUTH_TOKEN, token);
|
||||||
xhr.responseType = 'json';
|
xhr.responseType = 'json';
|
||||||
xhr.upload.onprogress = e => {
|
xhr.upload.onprogress = e => {
|
||||||
let percentComplete = Math.ceil((e.loaded / e.total) * 100);
|
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 = () => {
|
xhr.onload = () => {
|
||||||
|
window.store.dispatch(doUpdateUploadProgress(undefined, params));
|
||||||
resolve(xhr);
|
resolve(xhr);
|
||||||
};
|
};
|
||||||
xhr.onerror = () => {
|
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);
|
xhr.send(body);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return makeRequest(connectionString, 'POST', token, body)
|
return makeRequest(connectionString, 'POST', token, body, params)
|
||||||
.then(xhr => {
|
.then(xhr => {
|
||||||
let error;
|
let error;
|
||||||
if (xhr) {
|
if (xhr) {
|
||||||
|
@ -55,7 +70,7 @@ export default function apiPublishCallViaWeb(
|
||||||
} else if (xhr.statusText) {
|
} else if (xhr.statusText) {
|
||||||
error = new Error(xhr.statusText);
|
error = new Error(xhr.statusText);
|
||||||
} else {
|
} 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.'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
doFetchAccessToken,
|
doFetchAccessToken,
|
||||||
selectAccessToken,
|
selectAccessToken,
|
||||||
selectGetSyncErrorMessage,
|
selectGetSyncErrorMessage,
|
||||||
|
selectUploadCount,
|
||||||
} from 'lbryinc';
|
} from 'lbryinc';
|
||||||
import { doFetchTransactions, doFetchChannelListMine, selectBalance } from 'lbry-redux';
|
import { doFetchTransactions, doFetchChannelListMine, selectBalance } from 'lbry-redux';
|
||||||
import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings';
|
import { makeSelectClientSetting, selectThemePath } from 'redux/selectors/settings';
|
||||||
|
@ -26,6 +27,7 @@ const select = state => ({
|
||||||
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
syncEnabled: makeSelectClientSetting(SETTINGS.ENABLE_SYNC)(state),
|
||||||
syncError: selectGetSyncErrorMessage(state),
|
syncError: selectGetSyncErrorMessage(state),
|
||||||
accessToken: selectAccessToken(state),
|
accessToken: selectAccessToken(state),
|
||||||
|
uploadCount: selectUploadCount(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
|
|
@ -47,6 +47,7 @@ type Props = {
|
||||||
checkSync: () => void,
|
checkSync: () => void,
|
||||||
setSyncEnabled: boolean => void,
|
setSyncEnabled: boolean => void,
|
||||||
syncEnabled: boolean,
|
syncEnabled: boolean,
|
||||||
|
uploadCount: number,
|
||||||
balance: ?number,
|
balance: ?number,
|
||||||
accessToken: ?string,
|
accessToken: ?string,
|
||||||
syncError: ?string,
|
syncError: ?string,
|
||||||
|
@ -68,6 +69,7 @@ function App(props: Props) {
|
||||||
setSyncEnabled,
|
setSyncEnabled,
|
||||||
syncEnabled,
|
syncEnabled,
|
||||||
checkSync,
|
checkSync,
|
||||||
|
uploadCount,
|
||||||
balance,
|
balance,
|
||||||
accessToken,
|
accessToken,
|
||||||
history,
|
history,
|
||||||
|
@ -128,6 +130,16 @@ function App(props: Props) {
|
||||||
});
|
});
|
||||||
}, [balance, accessToken, hasDeterminedIfNewUser, setHasDeterminedIfNewUser]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
ReactModal.setAppElement(appRef.current);
|
ReactModal.setAppElement(appRef.current);
|
||||||
fetchAccessToken();
|
fetchAccessToken();
|
||||||
|
|
|
@ -13,6 +13,8 @@ type Props = {
|
||||||
fileLabel?: string,
|
fileLabel?: string,
|
||||||
directoryLabel?: string,
|
directoryLabel?: string,
|
||||||
accept?: string,
|
accept?: string,
|
||||||
|
error?: string,
|
||||||
|
disabled?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
class FileSelector extends React.PureComponent<Props> {
|
class FileSelector extends React.PureComponent<Props> {
|
||||||
|
@ -48,7 +50,7 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
input: ?HTMLInputElement;
|
input: ?HTMLInputElement;
|
||||||
|
|
||||||
render() {
|
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 buttonLabel = type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory');
|
||||||
const placeHolder = currentPath || placeholder;
|
const placeHolder = currentPath || placeholder;
|
||||||
|
@ -58,10 +60,14 @@ class FileSelector extends React.PureComponent<Props> {
|
||||||
label={label}
|
label={label}
|
||||||
webkitdirectory="true"
|
webkitdirectory="true"
|
||||||
className="form-field--copyable"
|
className="form-field--copyable"
|
||||||
|
error={error}
|
||||||
|
disabled={disabled}
|
||||||
type="text"
|
type="text"
|
||||||
readOnly="readonly"
|
readOnly="readonly"
|
||||||
value={placeHolder || __('Choose a file')}
|
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
|
<input
|
||||||
type={'file'}
|
type={'file'}
|
||||||
|
|
|
@ -8,6 +8,7 @@ const select = state => ({
|
||||||
filePath: makeSelectPublishFormValue('filePath')(state),
|
filePath: makeSelectPublishFormValue('filePath')(state),
|
||||||
isStillEditing: selectIsStillEditing(state),
|
isStillEditing: selectIsStillEditing(state),
|
||||||
balance: selectBalance(state),
|
balance: selectBalance(state),
|
||||||
|
publishing: makeSelectPublishFormValue('publishing')(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = dispatch => ({
|
const perform = dispatch => ({
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// @flow
|
// @flow
|
||||||
import * as ICONS from 'constants/icons';
|
import * as ICONS from 'constants/icons';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { regexInvalidURI } from 'lbry-redux';
|
import { regexInvalidURI } from 'lbry-redux';
|
||||||
import FileSelector from 'component/common/file-selector';
|
import FileSelector from 'component/common/file-selector';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import Card from 'component/common/card';
|
import Card from 'component/common/card';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
name: ?string,
|
name: ?string,
|
||||||
|
@ -13,10 +14,14 @@ type Props = {
|
||||||
balance: number,
|
balance: number,
|
||||||
updatePublishForm: ({}) => void,
|
updatePublishForm: ({}) => void,
|
||||||
disabled: boolean,
|
disabled: boolean,
|
||||||
|
publishing: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
function PublishFile(props: Props) {
|
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 = '';
|
let currentFile = '';
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
|
@ -30,29 +35,57 @@ function PublishFile(props: Props) {
|
||||||
function handleFileChange(file: WebFile) {
|
function handleFileChange(file: WebFile) {
|
||||||
// if electron, we'll set filePath to the path string because SDK is handling publishing.
|
// 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
|
// 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 } = {
|
const publishFormParams: { filePath: string | WebFile, name?: string } = {
|
||||||
filePath: file.path || file,
|
filePath: file.path || file,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
};
|
};
|
||||||
const parsedFileName = file.name.replace(regexInvalidURI, '');
|
const parsedFileName = file.name.replace(regexInvalidURI, '');
|
||||||
|
|
||||||
publishFormParams.name = parsedFileName.replace(' ', '-');
|
publishFormParams.name = parsedFileName.replace(' ', '-');
|
||||||
|
|
||||||
updatePublishForm(publishFormParams);
|
updatePublishForm(publishFormParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let title;
|
||||||
|
if (publishing) {
|
||||||
|
title = (
|
||||||
|
<span>
|
||||||
|
{__('Publishing')}
|
||||||
|
<Spinner type={'small'} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
title = isStillEditing ? __('Edit') : __('Publish');
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
icon={ICONS.PUBLISH}
|
icon={ICONS.PUBLISH}
|
||||||
disabled={disabled || balance === 0}
|
disabled={disabled || balance === 0}
|
||||||
title={isStillEditing ? __('Edit') : __('Publish')}
|
title={title}
|
||||||
subtitle={
|
subtitle={
|
||||||
isStillEditing ? __('You are currently editing a claim.') : __('Publish something totally wacky and wild.')
|
isStillEditing ? __('You are currently editing a claim.') : __('Publish something totally wacky and wild.')
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<FileSelector currentPath={currentFile} onFileChosen={handleFileChange} />
|
<FileSelector
|
||||||
|
disabled={disabled}
|
||||||
|
currentPath={currentFile}
|
||||||
|
onFileChosen={handleFileChange}
|
||||||
|
error={fileError}
|
||||||
|
/>
|
||||||
{!isStillEditing && (
|
{!isStillEditing && (
|
||||||
<p className="help">
|
<p className="help">
|
||||||
{__('For video content, use MP4s in H264/AAC format for best compatibility.')}{' '}
|
{__('For video content, use MP4s in H264/AAC format for best compatibility.')}{' '}
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
// @flow
|
// @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 React, { useEffect, Fragment } from 'react';
|
||||||
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
|
import { CHANNEL_NEW, CHANNEL_ANONYMOUS } from 'constants/claim';
|
||||||
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
|
import { buildURI, isURIValid, isNameValid, THUMBNAIL_STATUSES } from 'lbry-redux';
|
||||||
|
@ -122,7 +130,7 @@ function PublishForm(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<PublishFile disabled={formDisabled}/>
|
<PublishFile disabled={publishing} />
|
||||||
<div className={classnames({ 'card--disabled': formDisabled })}>
|
<div className={classnames({ 'card--disabled': formDisabled })}>
|
||||||
<PublishText disabled={formDisabled} />
|
<PublishText disabled={formDisabled} />
|
||||||
<Card actions={<SelectThumbnail />} />
|
<Card actions={<SelectThumbnail />} />
|
||||||
|
|
|
@ -24,7 +24,7 @@ function SelectAsset(props: Props) {
|
||||||
const { onUpdate, assetName, currentValue, recommended } = props;
|
const { onUpdate, assetName, currentValue, recommended } = props;
|
||||||
const [assetSource, setAssetSource] = useState(SOURCE_URL);
|
const [assetSource, setAssetSource] = useState(SOURCE_URL);
|
||||||
const [pathSelected, setPathSelected] = useState('');
|
const [pathSelected, setPathSelected] = useState('');
|
||||||
const [fileSelected, setFileSelected] = useState(null);
|
const [fileSelected, setFileSelected] = useState('');
|
||||||
const [uploadStatus, setUploadStatus] = useState(SPEECH_READY);
|
const [uploadStatus, setUploadStatus] = useState(SPEECH_READY);
|
||||||
|
|
||||||
function doUploadAsset(file) {
|
function doUploadAsset(file) {
|
||||||
|
@ -95,7 +95,7 @@ function SelectAsset(props: Props) {
|
||||||
button={'secondary'}
|
button={'secondary'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setPathSelected('');
|
setPathSelected('');
|
||||||
setFileSelected(null);
|
setFileSelected('');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
|
|
|
@ -2,7 +2,7 @@ import * as SETTINGS from 'constants/settings';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
import { selectSubscriptions } from 'redux/selectors/subscriptions';
|
||||||
import { selectFollowedTags } from 'lbry-redux';
|
import { selectFollowedTags } from 'lbry-redux';
|
||||||
import { selectUserEmail } from 'lbryinc';
|
import { selectUserEmail, selectUploadCount } from 'lbryinc';
|
||||||
import SideBar from './view';
|
import SideBar from './view';
|
||||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ const select = state => ({
|
||||||
followedTags: selectFollowedTags(state),
|
followedTags: selectFollowedTags(state),
|
||||||
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
language: makeSelectClientSetting(SETTINGS.LANGUAGE)(state), // trigger redraw on language change
|
||||||
email: selectUserEmail(state),
|
email: selectUserEmail(state),
|
||||||
|
uploadCount: selectUploadCount(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
const perform = () => ({});
|
const perform = () => ({});
|
||||||
|
|
|
@ -6,16 +6,18 @@ import Button from 'component/button';
|
||||||
import Tag from 'component/tag';
|
import Tag from 'component/tag';
|
||||||
import StickyBox from 'react-sticky-box/dist/esnext';
|
import StickyBox from 'react-sticky-box/dist/esnext';
|
||||||
import 'css-doodle';
|
import 'css-doodle';
|
||||||
|
import Spinner from 'component/spinner';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
subscriptions: Array<Subscription>,
|
subscriptions: Array<Subscription>,
|
||||||
followedTags: Array<Tag>,
|
followedTags: Array<Tag>,
|
||||||
email: ?string,
|
email: ?string,
|
||||||
obscureSideBar: boolean,
|
obscureSideBar: boolean,
|
||||||
|
uploadCount: number,
|
||||||
};
|
};
|
||||||
|
|
||||||
function SideBar(props: Props) {
|
function SideBar(props: Props) {
|
||||||
const { subscriptions, followedTags, obscureSideBar } = props;
|
const { subscriptions, followedTags, obscureSideBar, uploadCount } = props;
|
||||||
function buildLink(path, label, icon, guide) {
|
function buildLink(path, label, icon, guide) {
|
||||||
return {
|
return {
|
||||||
navigate: path ? `$/${path}` : '/',
|
navigate: path ? `$/${path}` : '/',
|
||||||
|
@ -52,7 +54,18 @@ function SideBar(props: Props) {
|
||||||
...buildLink(PAGES.CHANNELS, __('Channels'), ICONS.CHANNEL),
|
...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 => (
|
].map(linkProps => (
|
||||||
<li key={linkProps.label}>
|
<li key={linkProps.label}>
|
||||||
|
|
13
src/ui/component/webUploadList/index.js
Normal file
13
src/ui/component/webUploadList/index.js
Normal 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);
|
41
src/ui/component/webUploadList/internal/web-upload-item.jsx
Normal file
41
src/ui/component/webUploadList/internal/web-upload-item.jsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
37
src/ui/component/webUploadList/view.jsx
Normal file
37
src/ui/component/webUploadList/view.jsx
Normal 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>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
1
src/ui/constants/token.js
Normal file
1
src/ui/constants/token.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const X_LBRY_AUTH_TOKEN = 'X-Lbry-Auth-Token';
|
|
@ -27,6 +27,12 @@ class ModalPublishSuccess extends React.PureComponent<Props> {
|
||||||
navigate('/$/published');
|
navigate('/$/published');
|
||||||
closeModal();
|
closeModal();
|
||||||
}}
|
}}
|
||||||
|
confirmButtonLabel={'Show me!'}
|
||||||
|
abortButtonLabel={'Thanks!'}
|
||||||
|
onAborted={() => {
|
||||||
|
clearPublish();
|
||||||
|
closeModal();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<p>{__(`Your ${publishMessage} published to LBRY at the address`)}</p>
|
<p>{__(`Your ${publishMessage} published to LBRY at the address`)}</p>
|
||||||
<blockquote>{uri}</blockquote>
|
<blockquote>{uri}</blockquote>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import ClaimList from 'component/claimList';
|
||||||
import Page from 'component/page';
|
import Page from 'component/page';
|
||||||
import Paginate from 'component/common/paginate';
|
import Paginate from 'component/common/paginate';
|
||||||
import { PAGE_SIZE } from 'constants/claim';
|
import { PAGE_SIZE } from 'constants/claim';
|
||||||
|
import WebUploadList from 'component/webUploadList';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
checkPendingPublishes: () => void,
|
checkPendingPublishes: () => void,
|
||||||
|
@ -25,6 +26,7 @@ function FileListPublished(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page notContained>
|
<Page notContained>
|
||||||
|
<WebUploadList />
|
||||||
{urls && urls.length ? (
|
{urls && urls.length ? (
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<ClaimList
|
<ClaimList
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
homepageReducer,
|
homepageReducer,
|
||||||
statsReducer,
|
statsReducer,
|
||||||
syncReducer,
|
syncReducer,
|
||||||
|
lbrytvReducer,
|
||||||
} from 'lbryinc';
|
} from 'lbryinc';
|
||||||
import appReducer from 'redux/reducers/app';
|
import appReducer from 'redux/reducers/app';
|
||||||
import availabilityReducer from 'redux/reducers/availability';
|
import availabilityReducer from 'redux/reducers/availability';
|
||||||
|
@ -54,4 +55,5 @@ export default history =>
|
||||||
user: userReducer,
|
user: userReducer,
|
||||||
wallet: walletReducer,
|
wallet: walletReducer,
|
||||||
sync: syncReducer,
|
sync: syncReducer,
|
||||||
|
lbrytv: lbrytvReducer,
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,8 @@ import path from 'path';
|
||||||
import * as ACTIONS from 'constants/action_types';
|
import * as ACTIONS from 'constants/action_types';
|
||||||
import * as MODALS from 'constants/modal_types';
|
import * as MODALS from 'constants/modal_types';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
|
import { X_LBRY_AUTH_TOKEN } from 'constants/token';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
import {
|
import {
|
||||||
Lbry,
|
Lbry,
|
||||||
doBalanceSubscribe,
|
doBalanceSubscribe,
|
||||||
|
@ -437,8 +439,9 @@ export function doAnalyticsView(uri, timeToStart) {
|
||||||
export function doSignIn() {
|
export function doSignIn() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
// @if TARGET='web'
|
// @if TARGET='web'
|
||||||
const authToken = getAuthToken();
|
const { auth_token: authToken } = cookie.parse(document.cookie);
|
||||||
Lbry.setApiHeader(HEADERS.AUTH_TOKEN, authToken);
|
Lbry.setApiHeader(X_LBRY_AUTH_TOKEN, authToken);
|
||||||
|
|
||||||
dispatch(doBalanceSubscribe());
|
dispatch(doBalanceSubscribe());
|
||||||
dispatch(doFetchChannelListMine());
|
dispatch(doFetchChannelListMine());
|
||||||
// @endif
|
// @endif
|
||||||
|
|
|
@ -198,3 +198,36 @@ $border-color--dark: var(--dm-color-04);
|
||||||
flex: 1;
|
flex: 1;
|
||||||
font-size: var(--font-subtext);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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.",
|
"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:",
|
"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%.",
|
"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",
|
"Your Channels": "Your Channels",
|
||||||
"Add Tags": "Add Tags",
|
"Add Tags": "Add Tags",
|
||||||
"Available Balance": "Available Balance",
|
"Available Balance": "Available Balance",
|
||||||
|
|
Loading…
Reference in a new issue