Implement fallback to download if navigator.share() fails. Refactor ReceiveFileDialog

This commit is contained in:
schlagmichdoch 2024-02-08 01:36:20 +01:00
parent d8908e01ea
commit 19d33e11d8
2 changed files with 88 additions and 64 deletions

View file

@ -161,7 +161,9 @@
"message-transfer-completed": "Message transfer completed", "message-transfer-completed": "Message transfer completed",
"unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?", "unfinished-transfers-warning": "There are unfinished transfers. Are you sure you want to close PairDrop?",
"rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.", "rate-limit-join-key": "Rate limit reached. Wait 10 seconds and try again.",
"selected-peer-left": "Selected peer left" "selected-peer-left": "Selected peer left",
"error-sharing-size": "Files too big to be shared. They can be downloaded instead.",
"error-sharing-default": "Error while sharing. It can be downloaded instead."
}, },
"document-titles": { "document-titles": {
"file-received": "File Received", "file-received": "File Received",

View file

@ -1073,65 +1073,74 @@ class ReceiveFileDialog extends ReceiveDialog {
this.$shareBtn = this.$el.querySelector('#share-btn'); this.$shareBtn = this.$el.querySelector('#share-btn');
Events.on('files-received', e => this._onFilesReceived(e.detail.peerId, e.detail.files, e.detail.imagesOnly, e.detail.totalSize)); Events.on('files-received', e => this._onFilesReceived(e.detail.peerId, e.detail.files, e.detail.imagesOnly, e.detail.totalSize));
this._filesQueue = []; this._filesDataQueue = [];
} }
async _onFilesReceived(peerId, files, imagesOnly, totalSize) { async _onFilesReceived(peerId, files, imagesOnly, totalSize) {
const descriptor = this._getDescriptor(files, imagesOnly);
const displayName = $(peerId).ui._displayName(); const displayName = $(peerId).ui._displayName();
const connectionHash = $(peerId).ui._connectionHash; const connectionHash = $(peerId).ui._connectionHash;
const badgeClassName = $(peerId).ui._badgeClassName(); const badgeClassName = $(peerId).ui._badgeClassName();
this._filesQueue.push({ this._filesDataQueue.push({
peerId: peerId, peerId: peerId,
displayName: displayName,
connectionHash: connectionHash,
files: files, files: files,
imagesOnly: imagesOnly, imagesOnly: imagesOnly,
totalSize: totalSize, totalSize: totalSize,
descriptor: descriptor,
displayName: displayName,
connectionHash: connectionHash,
badgeClassName: badgeClassName badgeClassName: badgeClassName
}); });
audioPlayer.playBlop(); audioPlayer.playBlop();
await this._nextFiles(); await this._processFiles();
}
async _nextFiles() {
if (this._busy || !this._filesQueue.length) return;
this._busy = true;
const {peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName} = this._filesQueue.shift();
await this._displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName);
} }
canShareFilesViaMenu(files) { canShareFilesViaMenu(files) {
return window.isMobile && !!navigator.share && navigator.canShare({files}); return window.isMobile && !!navigator.share && navigator.canShare({files});
} }
async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName) { async _processFiles() {
const descriptor = this._getDescriptor(files, imagesOnly); if (this._busy || !this._filesDataQueue.length) return;
const documentTitleTranslation = files.length === 1
this._busy = true;
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
this._data = this._filesDataQueue.shift();
const documentTitleTranslation = this._data.files.length === 1
? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop` ? `${ Localization.getTranslation("document-titles.file-received") } - PairDrop`
: `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: files.length}) } - PairDrop`; : `${ Localization.getTranslation("document-titles.file-received-plural", null, {count: this._data.files.length}) } - PairDrop`;
// If possible, share via menu - else download files // If possible, share via menu - else download files
const shareViaMenu = this.canShareFilesViaMenu(files); const shareViaMenu = this.canShareFilesViaMenu(this._data.files);
this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize, badgeClassName); this._parseFileData(
this._setTitle(descriptor); this._data.displayName,
this._data.connectionHash,
this._data.files,
this._data.imagesOnly,
this._data.totalSize,
this._data.badgeClassName
);
this._setTitle(this._data.descriptor);
await this._addFileToPreviewBox(files[0]); await this._addFileToPreviewBox(this._data.files[0]);
document.title = documentTitleTranslation; document.title = documentTitleTranslation;
changeFavicon("images/favicon-96x96-notification.png"); changeFavicon("images/favicon-96x96-notification.png");
if (shareViaMenu) { if (shareViaMenu) {
await this._setViaShareMenu(files); await this._setupShareMenu();
} }
else { else {
await this._setViaDownload(peerId, files, totalSize, descriptor); await this._setupDownload();
} }
Events.fire('set-progress', {peerId: peerId, progress: 1, status: "receive-complete"}); Events.fire('set-progress', {peerId: this._data.peerId, progress: 0, status: "receive-complete"});
} }
_getDescriptor(files, imagesOnly) { _getDescriptor(files, imagesOnly) {
@ -1152,6 +1161,7 @@ class ReceiveFileDialog extends ReceiveDialog {
_setTitle(descriptor) { _setTitle(descriptor) {
this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor}); this.$receiveTitle.innerText = Localization.getTranslation("dialogs.receive-title", null, {descriptor: descriptor});
} }
createPreviewElement(file) { createPreviewElement(file) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
try { try {
@ -1200,13 +1210,22 @@ class ReceiveFileDialog extends ReceiveDialog {
}, duration); }, duration);
} }
async _setShareButton(files) { async _setShareButton() {
this.$shareBtn.onclick = _ => { this.$shareBtn.onclick = _ => {
navigator.share({files: files}) navigator.share({files: this._data.files})
.catch(err => { .catch(async err => {
Logger.error(err); Logger.error(err);
// Todo: tidy up, setDownloadButton instead and show warning to user
// Differentiate: "File too big to be shared. It can be downloaded instead." and "Error while sharing. It can be downloaded instead." if (err.name === 'AbortError' && err.message === 'Abort due to error while reading files.') {
Events.fire('notify-user', Localization.getTranslation("notifications.error-sharing-size"));
}
else {
Events.fire('notify-user', Localization.getTranslation("notifications.error-sharing-default"));
}
// Fallback to download
this._tidyUpButtons();
await this._setupDownload()
}); });
// Prevent clicking the button multiple times // Prevent clicking the button multiple times
@ -1216,42 +1235,28 @@ class ReceiveFileDialog extends ReceiveDialog {
this.$shareBtn.removeAttribute('hidden'); this.$shareBtn.removeAttribute('hidden');
} }
async _setDownloadButton(peerId, files, totalSize, descriptor) { async _processDataAsZip() {
let downloadTranslation = Localization.getTranslation("dialogs.download")
let downloadSuccessfulTranslation = Localization.getTranslation("notifications.download-successful", null, {descriptor: descriptor});
this.$downloadBtn.innerText = downloadTranslation;
this.$downloadBtn.removeAttribute('disabled');
this.$downloadBtn.removeAttribute('hidden');
let zipFileUrl, zipFileName; let zipFileUrl, zipFileName;
let sendAsZip = false;
const tooBigToZip = window.iOS && totalSize > 256000000; const tooBigToZip = window.iOS && this._data.totalSize > 256000000;
this.sendAsZip = false; if (this._data.files.length > 1 && !tooBigToZip) {
if (files.length > 1 && !tooBigToZip) { zipFileUrl = await this._createZipFile(this._data.files, zipProgress => {
Events.fire('set-progress', {peerId: this._peerId, progress: 0, status: 'process'});
zipFileUrl = await this._createZipFile(files, zipProgress => {
Events.fire('set-progress', { Events.fire('set-progress', {
peerId: peerId, peerId: this._data.peerId,
progress: zipProgress / totalSize, progress: zipProgress / this._data.totalSize,
status: 'process' status: 'process'
}) })
}); });
zipFileName = this._createZipFilename() zipFileName = this._createZipFilename()
this.sendAsZip = !!zipFileUrl; sendAsZip = !!zipFileUrl;
}
return {sendAsZip, zipFileUrl, zipFileName};
} }
// If single file or zipping failed -> download files individually -> else download zip _setDownloadButtonToZip(zipFileUrl, zipFileName) {
if (this.sendAsZip) { const downloadSuccessfulTranslation = Localization.getTranslation("notifications.download-successful", null, {descriptor: this._data.descriptor});
this._setDownloadButtonToZip(zipFileUrl, zipFileName, downloadSuccessfulTranslation);
} else {
this._setDownloadButtonToFiles(files, downloadSuccessfulTranslation, downloadTranslation);
}
}
_setDownloadButtonToZip(zipFileUrl, zipFileName, downloadSuccessfulTranslation) {
this.downloadSuccessful = false; this.downloadSuccessful = false;
this.$downloadBtn.onclick = _ => { this.$downloadBtn.onclick = _ => {
this._downloadFileFromUrl(zipFileUrl, zipFileName) this._downloadFileFromUrl(zipFileUrl, zipFileName)
@ -1263,12 +1268,17 @@ class ReceiveFileDialog extends ReceiveDialog {
}; };
} }
_setDownloadButtonToFiles(files, downloadSuccessfulTranslation, downloadTranslation) { _setDownloadButtonToFiles(files) {
const downloadTranslation = Localization.getTranslation("dialogs.download");
const downloadSuccessfulTranslation = Localization.getTranslation("notifications.download-successful", null, {descriptor: this._data.descriptor});
this.$downloadBtn.innerText = files.length === 1
? downloadTranslation
: `${downloadTranslation} 1/${files.length}`;
this.downloadSuccessful = false; this.downloadSuccessful = false;
let i = 0; let i = 0;
this.$downloadBtn.innerText = `${downloadTranslation} ${i + 1}/${files.length}`;
this.$downloadBtn.onclick = _ => { this.$downloadBtn.onclick = _ => {
this._disableButton(this.$shareBtn, 2000); this._disableButton(this.$shareBtn, 2000);
@ -1351,21 +1361,33 @@ class ReceiveFileDialog extends ReceiveDialog {
return `PairDrop_files_${year}${month}${date}_${hours}${minutes}.zip`; return `PairDrop_files_${year}${month}${date}_${hours}${minutes}.zip`;
} }
async _setViaShareMenu(files) { async _setupShareMenu() {
await this._setShareButton(files); await this._setShareButton();
// always show dialog // always show dialog
this.show(); this.show();
// open share menu automatically // open share menu automatically
setTimeout(() => { setTimeout(() => {
this.$shareBtn.click(); this.$shareBtn.click();
}, 500); }, 500);
} }
async _setViaDownload(peerId, files, totalSize, descriptor) { async _setupDownload() {
await this._setDownloadButton(peerId, files, totalSize, descriptor); this.$downloadBtn.innerText = Localization.getTranslation("dialogs.download");
this.$downloadBtn.removeAttribute('disabled');
this.$downloadBtn.removeAttribute('hidden');
if (!this.sendAsZip && files.length !== 1) { let {sendAsZip, zipFileUrl, zipFileName} = await this._processDataAsZip();
// If single file or zipping failed -> download files individually -> else download zip
if (sendAsZip) {
this._setDownloadButtonToZip(zipFileUrl, zipFileName);
} else {
this._setDownloadButtonToFiles(this._data.files);
}
if (!sendAsZip) {
this.show(); this.show();
return; return;
} }
@ -1373,7 +1395,7 @@ class ReceiveFileDialog extends ReceiveDialog {
// download automatically if zipped or if only one file is received // download automatically if zipped or if only one file is received
this.$downloadBtn.click(); this.$downloadBtn.click();
// if automatic download fails -> show dialog // if automatic download fails -> show dialog after 1 s
setTimeout(() => { setTimeout(() => {
if (!this.downloadSuccessful) { if (!this.downloadSuccessful) {
this.show(); this.show();
@ -1402,7 +1424,7 @@ class ReceiveFileDialog extends ReceiveDialog {
this._busy = false; this._busy = false;
await this._nextFiles(); await this._processFiles();
}, 300); }, 300);
} }
} }