Centralize evaluation of URL parameters to clean up code and remove redundancies, and streamline Base64Dialog class

This commit is contained in:
schlagmichdoch 2023-12-05 18:57:31 +01:00
parent cb86ce0e39
commit 10b658e2e9
6 changed files with 195 additions and 179 deletions

View file

@ -521,8 +521,13 @@
<x-dialog id="base64-paste-dialog"> <x-dialog id="base64-paste-dialog">
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<div class="row center p2">
<h2 class="dialog-title"></h2>
</div>
<div class="row p2">
<button class="btn btn-rounded btn-grey center" id="base64-paste-btn" title="Paste"></button> <button class="btn btn-rounded btn-grey center" id="base64-paste-btn" title="Paste"></button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div> <div class="textarea" title="CMD/⌘ + V" contenteditable hidden></div>
</div>
<div class="row-reverse center btn-row"> <div class="row-reverse center btn-row">
<button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button> <button class="btn btn-rounded btn-grey" data-i18n-key="dialogs.close" data-i18n-attrs="text" close></button>
</div> </div>

View file

@ -80,9 +80,11 @@
"send": "Send", "send": "Send",
"receive-text-title": "Message Received", "receive-text-title": "Message Received",
"copy": "Copy", "copy": "Copy",
"base64-title-files": "Share Files",
"base64-title-text": "Share Text",
"base64-processing": "Processing…", "base64-processing": "Processing…",
"base64-tap-to-paste": "Tap here to paste {{type}}", "base64-tap-to-paste": "Tap here to share {{type}}",
"base64-paste-to-send": "Paste here to send {{type}}", "base64-paste-to-send": "Paste clipboard here to share {{type}}",
"base64-text": "text", "base64-text": "text",
"base64-files": "files", "base64-files": "files",
"file-other-description-image": "and 1 other image", "file-other-description-image": "and 1 other image",

View file

@ -59,6 +59,9 @@ class PairDrop {
await this.hydrate(); await this.hydrate();
console.log("UI hydrated."); console.log("UI hydrated.");
// Evaluate url params as soon as ws is connected
Events.on('ws-connected', _ => this.evaluateUrlParams(), {once: true});
} }
registerServiceWorker() { registerServiceWorker() {
@ -171,6 +174,44 @@ class PairDrop {
this.server = new ServerConnection(); this.server = new ServerConnection();
this.peers = new PeersManager(this.server); this.peers = new PeersManager(this.server);
} }
async evaluateUrlParams() {
// get url params
const urlParams = new URLSearchParams(window.location.search);
const hash = window.location.hash.substring(1);
// evaluate url params
if (urlParams.has('pair_key')) {
const pairKey = urlParams.get('pair_key');
this.pairDeviceDialog._pairDeviceJoin(pairKey);
}
else if (urlParams.has('room_id')) {
const roomId = urlParams.get('room_id');
this.publicRoomDialog._joinPublicRoom(roomId);
}
else if (urlParams.has('base64text')) {
const base64Text = urlParams.get('base64text');
await this.base64Dialog.evaluateBase64Text(base64Text, hash);
}
else if (urlParams.has('base64zip')) {
const base64Zip = urlParams.get('base64zip');
await this.base64Dialog.evaluateBase64Zip(base64Zip, hash);
}
else if (urlParams.has("share_target")) {
const shareTargetType = urlParams.get("share_target");
const title = urlParams.get('title') || '';
const text = urlParams.get('text') || '';
const url = urlParams.get('url') || '';
await this.webShareTargetUI.evaluateShareTarget(shareTargetType, title, text, url);
}
else if (urlParams.has("file_handler")) {
await this.webFileHandlersUI.evaluateLaunchQueue();
}
// remove url params from url
const urlWithoutParams = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", urlWithoutParams);
}
} }
const pairDrop = new PairDrop(); const pairDrop = new PairDrop();

View file

@ -1321,8 +1321,6 @@ class PairDeviceDialog extends Dialog {
this.$el.addEventListener('paste', e => this._onPaste(e)); this.$el.addEventListener('paste', e => this._onPaste(e));
this.$qrCode.addEventListener('click', _ => this._copyPairUrl()); this.$qrCode.addEventListener('click', _ => this._copyPairUrl());
this.evaluateUrlAttributes();
this.pairPeer = {}; this.pairPeer = {};
} }
@ -1344,15 +1342,6 @@ class PairDeviceDialog extends Dialog {
this.inputKeyContainer._onPaste(pastedKey); this.inputKeyContainer._onPaste(pastedKey);
} }
evaluateUrlAttributes() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('pair_key')) {
this._pairDeviceJoin(urlParams.get('pair_key'));
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url); //remove pair_key from url
}
}
_pairDeviceInitiate() { _pairDeviceInitiate() {
Events.fire('pair-device-initiate'); Events.fire('pair-device-initiate');
} }
@ -1700,8 +1689,6 @@ class PublicRoomDialog extends Dialog {
this.$el.addEventListener('paste', e => this._onPaste(e)); this.$el.addEventListener('paste', e => this._onPaste(e));
this.$qrCode.addEventListener('click', _ => this._copyShareRoomUrl()); this.$qrCode.addEventListener('click', _ => this._copyShareRoomUrl());
this.evaluateUrlAttributes();
Events.on('ws-connected', _ => this._onWsConnected()); Events.on('ws-connected', _ => this._onWsConnected());
Events.on('translation-loaded', _ => this.setFooterBadge()); Events.on('translation-loaded', _ => this.setFooterBadge());
} }
@ -1791,15 +1778,6 @@ class PublicRoomDialog extends Dialog {
}) })
} }
evaluateUrlAttributes() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('room_id')) {
this._joinPublicRoom(urlParams.get('room_id'));
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url); //remove pair_key from url
}
}
_onWsConnected() { _onWsConnected() {
let roomId = sessionStorage.getItem('public_room_id'); let roomId = sessionStorage.getItem('public_room_id');
@ -2147,61 +2125,47 @@ class Base64Dialog extends Dialog {
constructor() { constructor() {
super('base64-paste-dialog'); super('base64-paste-dialog');
const urlParams = new URL(window.location).searchParams;
const base64Text = urlParams.get('base64text');
const base64Zip = urlParams.get('base64zip');
const base64Hash = window.location.hash.substring(1);
this.$title = this.$el.querySelector('.dialog-title');
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea'); this.$fallbackTextarea = this.$el.querySelector('.textarea');
}
async evaluateBase64Text(base64Text, hash) {
this.$title.innerText = Localization.getTranslation('dialogs.base64-title-text');
if (base64Text) {
this.show();
if (base64Text === 'paste') { if (base64Text === 'paste') {
// ?base64text=paste // ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard // base64 encoded string is ready to be pasted from clipboard
this.preparePasting('text'); this.preparePasting('text');
this.show();
} }
else if (base64Text === 'hash') { else if (base64Text === 'hash') {
// ?base64text=hash#BASE64ENCODED // ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended) // base64 encoded text is url hash which cannot be seen by the server and is faster (recommended)
this.processBase64Text(base64Hash) this.show();
.catch(_ => { await this.processBase64Text(hash)
Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect.");
}).finally(() => {
this.hide();
});
} }
else { else {
// ?base64text=BASE64ENCODED // ?base64text=BASE64ENCODED
// base64 encoded string was part of url param (not recommended) // base64 encoded text is part of the url param. Seen by server and slow (not recommended)
this.processBase64Text(base64Text)
.catch(_ => {
Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect.");
}).finally(() => {
this.hide();
});
}
}
else if (base64Zip) {
this.show(); this.show();
if (base64Zip === "hash") { await this.processBase64Text(base64Text)
// ?base64zip=hash#BASE64ENCODED
// base64 encoded zip file is url hash which is never sent to the server
this.processBase64Zip(base64Hash)
.catch(_ => {
Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect"));
console.log("File content incorrect.");
}).finally(() => {
this.hide();
});
} }
else { }
async evaluateBase64Zip(base64Zip, hash) {
this.$title.innerText = Localization.getTranslation('dialogs.base64-title-files');
if (base64Zip === 'paste') {
// ?base64zip=paste || ?base64zip=true // ?base64zip=paste || ?base64zip=true
this.preparePasting('files'); this.preparePasting('files');
this.show();
} }
else if (base64Zip === 'hash') {
// ?base64zip=hash#BASE64ENCODED
// base64 encoded zip file is url hash which cannot be seen by the server
await this.processBase64Zip(hash)
} }
} }
@ -2234,28 +2198,15 @@ class Base64Dialog extends Dialog {
async processInput(type) { async processInput(type) {
const base64 = this.$fallbackTextarea.textContent; const base64 = this.$fallbackTextarea.textContent;
this.$fallbackTextarea.textContent = ''; this.$fallbackTextarea.textContent = '';
await this.processBase64(type, base64); await this.processPastedBase64(type, base64);
} }
async processClipboard(type) { async processClipboard(type) {
const base64 = await navigator.clipboard.readText(); const base64 = await navigator.clipboard.readText();
await this.processBase64(type, base64); await this.processPastedBase64(type, base64);
} }
isValidBase64(base64) { async processPastedBase64(type, base64) {
try {
// check if input is base64 encoded
window.atob(base64);
return true;
} catch (e) {
// input is not base64 string.
return false;
}
}
async processBase64(type, base64) {
if (!base64 || !this.isValidBase64(base64)) return;
this._setPasteBtnToProcessing();
try { try {
if (type === 'text') { if (type === 'text') {
await this.processBase64Text(base64); await this.processBase64Text(base64);
@ -2263,51 +2214,50 @@ class Base64Dialog extends Dialog {
else { else {
await this.processBase64Zip(base64); await this.processBase64Zip(base64);
} }
} catch(_) { }
catch(e) {
Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect")); Events.fire('notify-user', Localization.getTranslation("notifications.clipboard-content-incorrect"));
console.log("Clipboard content is incorrect.") console.log("Clipboard content is incorrect.")
} }
this.hide(); this.hide();
} }
processBase64Text(base64Text){ async processBase64Text(base64){
return new Promise((resolve) => {
this._setPasteBtnToProcessing(); this._setPasteBtnToProcessing();
let decodedText = decodeURIComponent(escape(window.atob(base64Text)));
try {
const decodedText = await decodeBase64Text(base64);
if (ShareTextDialog.isApproveShareTextSet()) { if (ShareTextDialog.isApproveShareTextSet()) {
Events.fire('share-text-dialog', decodedText); Events.fire('share-text-dialog', decodedText);
} else { }
else {
Events.fire('activate-share-mode', {text: decodedText}); Events.fire('activate-share-mode', {text: decodedText});
} }
resolve(); }
}); catch (e) {
Events.fire('notify-user', Localization.getTranslation("notifications.text-content-incorrect"));
console.log("Text content incorrect.");
} }
async processBase64Zip(base64zip) { this.hide();
}
async processBase64Zip(base64) {
this._setPasteBtnToProcessing(); this._setPasteBtnToProcessing();
let bstr = atob(base64zip), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) { try {
u8arr[n] = bstr.charCodeAt(n); const decodedFiles = await decodeBase64Files(base64);
Events.fire('activate-share-mode', {files: decodedFiles});
}
catch (e) {
Events.fire('notify-user', Localization.getTranslation("notifications.file-content-incorrect"));
console.log("File content incorrect.");
} }
const zipBlob = new File([u8arr], 'archive.zip'); this.hide();
let files = [];
const zipEntries = await zipper.getEntries(zipBlob);
for (let i = 0; i < zipEntries.length; i++) {
let fileBlob = await zipper.getData(zipEntries[i]);
files.push(new File([fileBlob], zipEntries[i].filename));
}
Events.fire('activate-share-mode', {files: files});
}
clearBrowserHistory() {
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
} }
hide() { hide() {
this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback()); this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback()); this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide(); super.hide();
@ -2529,16 +2479,10 @@ class NetworkStatusUI {
} }
class WebShareTargetUI { class WebShareTargetUI {
constructor() {
const urlParams = new URL(window.location).searchParams;
const share_target_type = urlParams.get("share-target")
if (share_target_type) {
if (share_target_type === "text") {
const title = urlParams.get('title') || '';
const text = urlParams.get('text') || '';
const url = urlParams.get('url') || '';
let shareTargetText;
async evaluateShareTarget(shareTargetType, title, text, url) {
if (shareTargetType === "text") {
let shareTargetText;
if (url) { if (url) {
shareTargetText = url; // we share only the link - no text. shareTargetText = url; // we share only the link - no text.
} }
@ -2551,11 +2495,12 @@ class WebShareTargetUI {
if (ShareTextDialog.isApproveShareTextSet()) { if (ShareTextDialog.isApproveShareTextSet()) {
Events.fire('share-text-dialog', shareTargetText); Events.fire('share-text-dialog', shareTargetText);
} else { }
else {
Events.fire('activate-share-mode', {text: shareTargetText}); Events.fire('activate-share-mode', {text: shareTargetText});
} }
} }
else if (share_target_type === "files") { else if (shareTargetType === "files") {
let openRequest = window.indexedDB.open('pairdrop_store') let openRequest = window.indexedDB.open('pairdrop_store')
openRequest.onsuccess = e => { openRequest.onsuccess = e => {
const db = e.target.result; const db = e.target.result;
@ -2564,10 +2509,12 @@ class WebShareTargetUI {
const request = store.getAll(); const request = store.getAll();
request.onsuccess = _ => { request.onsuccess = _ => {
const fileObjects = request.result; const fileObjects = request.result;
let filesReceived = []; let filesReceived = [];
for (let i = 0; i < fileObjects.length; i++) { for (let i = 0; i < fileObjects.length; i++) {
filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name)); filesReceived.push(new File([fileObjects[i].buffer], fileObjects[i].name));
} }
const clearRequest = store.clear() const clearRequest = store.clear()
clearRequest.onsuccess = _ => db.close(); clearRequest.onsuccess = _ => db.close();
@ -2575,34 +2522,29 @@ class WebShareTargetUI {
} }
} }
} }
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
}
} }
} }
class WebFileHandlersUI { class WebFileHandlersUI {
constructor() { async evaluateLaunchQueue() {
const urlParams = new URL(window.location).searchParams; if (!"launchQueue" in window) return;
if (urlParams.has("file_handler") && "launchQueue" in window) {
launchQueue.setConsumer(async launchParams => { launchQueue.setConsumer(async launchParams => {
console.log("Launched with: ", launchParams); console.log("Launched with: ", launchParams);
if (!launchParams.files.length)
return; if (!launchParams.files.length) return;
let files = []; let files = [];
for (let i = 0; i < launchParams.files.length; i++) { for (let i = 0; i < launchParams.files.length; i++) {
if (i !== 0 && await launchParams.files[i].isSameEntry(launchParams.files[i-1])) continue; if (i !== 0 && await launchParams.files[i].isSameEntry(launchParams.files[i-1])) continue;
const fileHandle = launchParams.files[i];
const file = await fileHandle.getFile(); const file = await launchParams.files[i].getFile();
files.push(file); files.push(file);
} }
Events.fire('activate-share-mode', {files: files}) Events.fire('activate-share-mode', {files: files})
launchParams = null;
}); });
const url = getUrlWithoutArguments();
window.history.replaceState({}, "Rewrite URL", url);
}
} }
} }

View file

@ -505,3 +505,28 @@ function getResizedImageDataUrl(file, width = undefined, height = undefined, qua
image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`); image.onerror = _ => reject(`Could not create an image thumbnail from type ${file.type}`);
}) })
} }
async function decodeBase64Files(base64) {
if (!base64) throw new Error('Base64 is empty');
let bstr = atob(base64), n = bstr.length, u8arr = new Uint8Array(n);
while (n--) {
u8arr[n] = bstr.charCodeAt(n);
}
const zipBlob = new File([u8arr], 'archive.zip');
let files = [];
const zipEntries = await zipper.getEntries(zipBlob);
for (let i = 0; i < zipEntries.length; i++) {
let fileBlob = await zipper.getData(zipEntries[i]);
files.push(new File([fileBlob], zipEntries[i].filename));
}
return files
}
async function decodeBase64Text(base64) {
if (!base64) throw new Error('Base64 is empty');
return decodeURIComponent(escape(window.atob(base64)))
}

View file

@ -705,6 +705,7 @@ x-dialog .dialog-subheader {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; border: solid 12px #438cff;
margin: 6px;
} }
#base64-paste-dialog .textarea { #base64-paste-dialog .textarea {