lbry-desktop/ui/component/webUploadList/internal/web-upload-item.jsx
infinite-persistence 157b50c58e
Upload: tab sync and various fixes (#428)
* Upload: fix redux key clash

## Issue
`params` is the "final" value that will be passed to the SDK and  `channel` is not a valid argument (it should be `channel_name`). Also, it seems like we only pass the channel ID now and skip the channel name entirely.

For the anonymous case, a clash will still happen when since the channel part is hardcoded to `anonymous`.

## Approach
Generate a guid in `params` and use that as the key to handle all the cases above. We couldn't use the `uploadUrl` because v1 doesn't have it.

The old formula is retained to allow users to retry or cancel their existing uploads one last time (otherwise it will persist forever). The next upload will be using the new key.

* Upload: add tab-locking

## Issue
- The previous code does detect uploads from multiple tabs, but it was done by handling the CONFLICT error message from the backend. At certain corner-cases, this does not work well. A better way is to not allow resumption while the same file is being uploading from another tab.

- When an upload from 1 tab finishes, the GUI on the other tab does not remove the completed item. User either have to refresh or click Cancel. Clicking Cancel results in the 404 backend error. This should be avoided.

## Approach
- Added tab synchronization and locking by passing the "locked" and "removed" information through `localStorage`.

## Other considered approaches
- Wallet sync -- but decided not to pollute the wallet.
- 3rd-party redux tab syncing -- but decided it's not worth adding another module for 1 usage.

* Upload: check if locked before confirming delete

## Reproduce
Have 2 tabs + paused upload
Open "cancel" dialog in one of the tabs.
Continue upload in other tab
Confirm cancellation in first tab
Upload disappears from both tabs, but based on network traffic the upload keeps happening.
(If upload finishes the claim seems to get created)
2021-12-07 09:48:09 -05:00

179 lines
5.6 KiB
JavaScript

// @flow
import React, { useState } from 'react';
import FileSelector from 'component/common/file-selector';
import Button from 'component/button';
import FileThumbnail from 'component/fileThumbnail';
import * as MODALS from 'constants/modal_types';
import { serializeFileObj } from 'util/file';
import { tusIsSessionLocked } from 'util/tus';
type Props = {
uploadItem: FileUploadItem,
doPublishResume: (any) => void,
doUpdateUploadRemove: (string, any) => void,
doOpenModal: (string, {}) => void,
};
export default function WebUploadItem(props: Props) {
const { uploadItem, doPublishResume, doUpdateUploadRemove, doOpenModal } = props;
const { params, file, fileFingerprint, progress, status, resumable, uploader } = uploadItem;
const [showFileSelector, setShowFileSelector] = useState(false);
const locked = tusIsSessionLocked(params.guid);
function handleFileChange(newFile: WebFile, clearName = true) {
if (serializeFileObj(newFile) === fileFingerprint) {
setShowFileSelector(false);
doPublishResume({ ...params, file_path: newFile });
if (!params.guid) {
// Can remove this if-block after January 2022.
doUpdateUploadRemove('', params);
}
} else {
doOpenModal(MODALS.CONFIRM, {
title: __('Invalid file'),
subtitle: __('It appears to be a different or modified file.'),
body: <p className="help--warning">{__('Please select the same file from the initial upload.')}</p>,
onConfirm: (closeModal) => closeModal(),
hideCancel: true,
});
}
}
function handleCancel() {
doOpenModal(MODALS.CONFIRM, {
title: __('Cancel upload'),
subtitle: __('Cancel and remove the selected upload?'),
body: params.name ? <p className="empty">{`lbry://${params.name}`}</p> : undefined,
onConfirm: (closeModal) => {
if (tusIsSessionLocked(params.guid)) {
// Corner-case: it's possible for the upload to resume in another tab
// after the modal has appeared. Make a final lock-check here.
// We can invoke a toast here, but just do nothing for now.
// The upload status should make things obvious.
} else {
if (uploader) {
if (resumable) {
// $FlowFixMe - couldn't resolve to TusUploader manually.
uploader.abort(true); // TUS
} else {
uploader.abort(); // XHR
}
}
// The second parameter (params) can be removed after January 2022.
doUpdateUploadRemove(params.guid, params);
}
closeModal();
},
});
}
function resolveProgressStr() {
if (locked) {
return __('File being uploaded in another tab or window.');
}
if (!uploader) {
return __('Stopped.');
}
if (resumable) {
if (status) {
switch (status) {
case 'retry':
return __('Retrying...');
case 'error':
return __('Failed.');
case 'conflict':
return __('Stopped. Duplicate session detected.');
default:
return status;
}
} else {
const progressInt = parseInt(progress);
return progressInt === 100 ? __('Processing...') : __('Uploading...');
}
} else {
return __('Uploading...');
}
}
function getRetryButton() {
if (!resumable || locked) {
return null;
}
if (uploader) {
// Should still be uploading. Don't show.
return null;
} else {
// Refreshed or connection broken.
const isFileActive = file instanceof File;
return (
<Button
label={isFileActive ? __('Resume') : __('Retry')}
button="link"
onClick={() => {
if (isFileActive) {
doPublishResume({ ...params, file_path: file });
} else {
setShowFileSelector(true);
}
}}
disabled={showFileSelector}
/>
);
}
}
function getCancelButton() {
if (!locked) {
return <Button label={__('Cancel')} button="link" onClick={handleCancel} />;
}
}
function getFileSelector() {
return (
<div className="claim-preview--padded">
<FileSelector
label={__('File')}
onFileChosen={handleFileChange}
// https://stackoverflow.com/questions/19107685/safari-input-type-file-accept-video-ignores-mp4-files
accept={'video/mp4,video/x-m4v,video/*,audio/*'}
placeholder={__('Select the file to resume upload...')}
/>
</div>
);
}
function getProgressBar() {
return (
<>
<div className="claim-upload__progress--label">lbry://{params.name}</div>
<div className={'claim-upload__progress--outer card--inline'}>
<div className={'claim-upload__progress--inner'} style={{ width: `${progress}%` }}>
<span className="claim-upload__progress--inner-text">{resolveProgressStr()}</span>
</div>
</div>
</>
);
}
return (
<li className={'web-upload-item claim-preview claim-preview--padded claim-preview--inactive card--inline'}>
<FileThumbnail thumbnail={params.thumbnail_url} />
<div className={'claim-preview-metadata'}>
<div className="claim-preview-info">
<div className="claim-preview__title">{params.title}</div>
<div className="card__actions--inline">
{getRetryButton()}
{getCancelButton()}
</div>
</div>
{showFileSelector && getFileSelector()}
{!showFileSelector && getProgressBar()}
</div>
</li>
);
}