diff --git a/flow-typed/publish.js b/flow-typed/publish.js index 72042d33c..88957ddd6 100644 --- a/flow-typed/publish.js +++ b/flow-typed/publish.js @@ -62,7 +62,8 @@ declare type FileUploadSdkParams = { remote_url?: string, thumbnail_url?: string, title?: string, - // Temporary values + // Temporary values; remove when passing to SDK + guid: string, uploadUrl?: string, }; diff --git a/static/app-strings.json b/static/app-strings.json index 46815230d..e79a29143 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -1307,6 +1307,8 @@ "Uploading...": "Uploading...", "Creating...": "Creating...", "Stopped. Duplicate session detected.": "Stopped. Duplicate session detected.", + "File being uploaded in another tab or window.": "File being uploaded in another tab or window.", + "There are pending uploads.": "There are pending uploads.", "Use a URL": "Use a URL", "Edit Cover Image": "Edit Cover Image", "Cover Image": "Cover Image", diff --git a/ui/component/app/view.jsx b/ui/component/app/view.jsx index 3d3f065c1..0ebd85cfe 100644 --- a/ui/component/app/view.jsx +++ b/ui/component/app/view.jsx @@ -2,6 +2,7 @@ import * as PAGES from 'constants/pages'; import React, { useEffect, useRef, useState, useLayoutEffect } from 'react'; import { lazyImport } from 'util/lazyImport'; +import { tusUnlockAndNotify, tusHandleTabUpdates } from 'util/tus'; import classnames from 'classnames'; import analytics from 'analytics'; import { setSearchUserId } from 'redux/actions/search'; @@ -231,12 +232,29 @@ function App(props: Props) { useEffect(() => { if (!uploadCount) return; + + const handleUnload = (event) => tusUnlockAndNotify(); const handleBeforeUnload = (event) => { event.preventDefault(); - event.returnValue = 'magic'; // without setting this to something it doesn't work + event.returnValue = __('There are pending uploads.'); // without setting this to something it doesn't work }; + + window.addEventListener('unload', handleUnload); window.addEventListener('beforeunload', handleBeforeUnload); - return () => window.removeEventListener('beforeunload', handleBeforeUnload); + + return () => { + window.removeEventListener('unload', handleUnload); + window.removeEventListener('beforeunload', handleBeforeUnload); + }; + }, [uploadCount]); + + useEffect(() => { + if (!uploadCount) return; + + const onStorageUpdate = (e) => tusHandleTabUpdates(e.key); + window.addEventListener('storage', onStorageUpdate); + + return () => window.removeEventListener('storage', onStorageUpdate); }, [uploadCount]); // allows user to pause miniplayer using the spacebar without the page scrolling down diff --git a/ui/component/webUploadList/internal/web-upload-item.jsx b/ui/component/webUploadList/internal/web-upload-item.jsx index 24d7494c3..de9550950 100644 --- a/ui/component/webUploadList/internal/web-upload-item.jsx +++ b/ui/component/webUploadList/internal/web-upload-item.jsx @@ -5,11 +5,12 @@ 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: (any) => void, + doUpdateUploadRemove: (string, any) => void, doOpenModal: (string, {}) => void, }; @@ -18,11 +19,16 @@ export default function WebUploadItem(props: 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'), @@ -40,21 +46,34 @@ export default function WebUploadItem(props: Props) { subtitle: __('Cancel and remove the selected upload?'), body: params.name ?

{`lbry://${params.name}`}

: undefined, onConfirm: (closeModal) => { - if (uploader) { - if (resumable) { - // $FlowFixMe - couldn't resolve to TusUploader manually. - uploader.abort(true); // TUS - } else { - uploader.abort(); // XHR + 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); } - doUpdateUploadRemove(params); closeModal(); }, }); } function resolveProgressStr() { + if (locked) { + return __('File being uploaded in another tab or window.'); + } + if (!uploader) { return __('Stopped.'); } @@ -81,7 +100,7 @@ export default function WebUploadItem(props: Props) { } function getRetryButton() { - if (!resumable) { + if (!resumable || locked) { return null; } @@ -109,7 +128,9 @@ export default function WebUploadItem(props: Props) { } function getCancelButton() { - return