Compare commits

..

21 Commits

Author SHA1 Message Date
schlagmichdoch
fea15d3ee1 increase version to v1.4.5 2023-03-13 00:05:57 +01:00
schlagmichdoch
028752a809 fixes #76. 'File received' dialog not showing on iOS when big videos are sent. 2023-03-13 00:04:48 +01:00
schlagmichdoch
1093f4d246 log error onicecandidateerror 2023-03-10 22:21:19 +01:00
schlagmichdoch
7ddd600b0c fix display name hidden on Firefox for Android 2023-03-10 20:01:59 +01:00
schlagmichdoch
715356aafb Fix AirDrop typo 2023-03-08 11:35:37 +01:00
schlagmichdoch
490e4db380 Add information about specifying TURN servers 2023-03-07 18:25:25 +01:00
schlagmichdoch
11a988e550 increase version to v1.4.4 2023-03-06 16:05:58 +01:00
schlagmichdoch
ff8f28660a prevent buttons from submitting form by adding type="button" 2023-03-06 16:03:34 +01:00
schlagmichdoch
5fc8e85f75 increase version to 1.4.3 2023-03-06 15:40:09 +01:00
schlagmichdoch
5eeaae01fe add connection hash to title of display-name of receive dialogs 2023-03-06 15:39:24 +01:00
schlagmichdoch
660e523263 prevent sending of displayName if RTCPeer is not connected 2023-03-06 15:33:22 +01:00
schlagmichdoch
cdfbc7a2df add missing removal of event listener to ws fallback ui.js 2023-03-06 15:32:58 +01:00
schlagmichdoch
c9dca7e083 fix download notification and add request notification 2023-03-06 15:32:42 +01:00
schlagmichdoch
79af04d95a increase version to v1.4.2 2023-03-06 12:31:44 +01:00
schlagmichdoch
954e9c7c3a Merge pull request #65 from schlagmichdoch/pairdrop_cli_add_firefox_fallback
pairdrop-cli: add fallback if navigator.clipboard.readText() is not available
2023-03-06 12:25:54 +01:00
schlagmichdoch
c0d504f6a8 remove base64 event listeners manually on hide instead of once: true 2023-03-06 12:20:30 +01:00
schlagmichdoch
36e152dc7c add { once: true } to deactivate-paste-mode event listener 2023-03-06 11:59:56 +01:00
schlagmichdoch
fdf024f378 pairdrop-cli: add fallback if navigator.clipboard.readText() is not available 2023-03-06 11:56:17 +01:00
schlagmichdoch
9f2e4c5f8f fix displayName sometimes not exchanged on reload 2023-03-06 11:24:19 +01:00
schlagmichdoch
8e219914ec Merge pull request #66 from schlagmichdoch/dependabot/npm_and_yarn/ua-parser-js-1.0.34
Bump ua-parser-js from 1.0.33 to 1.0.34
2023-03-06 10:53:35 +01:00
dependabot[bot]
d1273ef9cc Bump ua-parser-js from 1.0.33 to 1.0.34
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.33 to 1.0.34.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/1.0.34/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/1.0.33...1.0.34)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 05:12:00 +00:00
14 changed files with 401 additions and 198 deletions

View File

@@ -6,7 +6,7 @@
<h1>PairDrop</h1>
<p>
Local file sharing in your browser. Inspired by Apple's Airdrop.
Local file sharing in your browser. Inspired by Apple's AirDrop.
<br />
<a href="https://pairdrop.net"><strong>Explore »</strong></a>
<br />

View File

@@ -58,8 +58,11 @@ If your devices are paired and behind a NAT, the public TURN Server from [Open R
Yes. Your files are sent using WebRTC, which encrypts them on transit. To ensure the connection is secure and there is no MITM, compare the security number shown under the device name on both devices. The security number is different for every connection.
### Transferring many files with paired devices takes too long
Naturally, if traffic needs to be routed through the turn server transfer speed decreases.
As a workaround you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster.
Naturally, if traffic needs to be routed through the turn server because your devices are behind different NATs, transfer speed decreases.
As the public TURN server used is not super fast, you can easily [specify to use your own TURN server](https://github.com/schlagmichdoch/PairDrop/blob/master/docs/host-your-own.md#specify-stunturn-servers) if you host your own instance.
Alternatively, you can open a hotspot on one of your devices to bridge the connection which makes transfers much faster as no TURN server is needed.
- [How to open a hotspot on Windows](https://support.microsoft.com/en-us/windows/use-your-windows-pc-as-a-mobile-hotspot-c89b0fad-72d5-41e8-f7ea-406ad9036b85#WindowsVersion=Windows_11)
- [How to open a hotspot on Mac](https://support.apple.com/guide/mac-help/share-internet-connection-mac-network-users-mchlp1540/mac)

18
package-lock.json generated
View File

@@ -1,17 +1,17 @@
{
"name": "pairdrop",
"version": "1.4.1",
"version": "1.4.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "pairdrop",
"version": "1.4.1",
"version": "1.4.5",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33",
"ua-parser-js": "^1.0.34",
"unique-names-generator": "^4.3.0",
"ws": "^8.12.1"
},
@@ -583,9 +583,9 @@
}
},
"node_modules/ua-parser-js": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==",
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
"funding": [
{
"type": "opencollective",
@@ -1070,9 +1070,9 @@
}
},
"ua-parser-js": {
"version": "1.0.33",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ=="
"version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
},
"unique-names-generator": {
"version": "4.7.1",

View File

@@ -1,6 +1,6 @@
{
"name": "pairdrop",
"version": "1.4.1",
"version": "1.4.5",
"description": "",
"main": "index.js",
"scripts": {
@@ -12,7 +12,7 @@
"dependencies": {
"express": "^4.18.2",
"express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33",
"ua-parser-js": "^1.0.34",
"unique-names-generator": "^4.3.0",
"ws": "^8.12.1"
},

View File

@@ -122,7 +122,7 @@
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button>
<button class="button" close>Cancel</button>
<button class="button" type="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -137,7 +137,7 @@
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" close>Cancel</button>
<button class="button" type="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -209,7 +209,7 @@
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" title="ESCAPE" close>Cancel</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -238,6 +238,7 @@
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button>
</x-paper>
</x-background>

View File

@@ -194,6 +194,10 @@ class Peer {
this._send(JSON.stringify(message));
}
sendDisplayName(displayName) {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
@@ -490,7 +494,8 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName) return;
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
}
}
@@ -519,6 +524,7 @@ class RTCPeer extends Peer {
this._peerId = peerId;
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
}
@@ -689,6 +695,11 @@ class RTCPeer extends Peer {
_isConnecting() {
return this._channel && this._channel.readyState === 'connecting';
}
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
}
}
class PeersManager {
@@ -707,6 +718,7 @@ class PeersManager {
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
}
_onMessage(message) {
@@ -793,7 +805,7 @@ class PeersManager {
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer) return;
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
this.peers[peerId].sendDisplayName(this._displayName);
}
_onDisplayName(displayName) {
@@ -887,11 +899,11 @@ class Events {
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
}
static on(type, callback) {
return window.addEventListener(type, callback, false);
static on(type, callback, options = false) {
return window.addEventListener(type, callback, options);
}
static off(type, callback) {
return window.removeEventListener(type, callback, false);
static off(type, callback, options = false) {
return window.removeEventListener(type, callback, options);
}
}

View File

@@ -241,7 +241,7 @@ class PeersUI {
const _callback = (e) => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback);
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback));
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
this.$cancelPasteModeBtn.removeAttribute('hidden');
@@ -300,7 +300,8 @@ class PeerUI {
constructor(peer, connectionHash) {
this._peer = peer;
this._connectionHash = connectionHash;
this._connectionHash =
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
this._initDom();
this._bindListeners();
@@ -345,8 +346,7 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent =
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
}
_initDom() {
@@ -569,7 +569,7 @@ class ReceiveDialog extends Dialog {
}
}
_parseFileData(displayName, files, imagesOnly, totalSize) {
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
@@ -586,6 +586,7 @@ class ReceiveDialog extends Dialog {
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
}
@@ -603,8 +604,9 @@ class ReceiveFileDialog extends ReceiveDialog {
}
_onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
const displayName = $(sender).ui._displayName();
const connectionHash = $(sender).ui._connectionHash;
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play();
}
@@ -612,12 +614,11 @@ class ReceiveFileDialog extends ReceiveDialog {
_nextFiles() {
if (this._busy) return;
this._busy = true;
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
}
_dequeueFile() {
// Todo: change count in document.title and move '- PairDrop' to back
if (!this._filesQueue.length) { // nothing to do
this._busy = false;
return;
@@ -631,32 +632,40 @@ class ReceiveFileDialog extends ReceiveDialog {
createPreviewElement(file) {
return new Promise((resolve, reject) => {
let mime = file.type.split('/')[0]
let previewElement = {
image: 'img',
audio: 'audio',
video: 'video'
}
try {
let mime = file.type.split('/')[0]
let previewElement = {
image: 'img',
audio: 'audio',
video: 'video'
}
if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false);
} else {
console.log('the file is able to preview');
let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file);
element.controls = true;
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true)
};
element.addEventListener('loadeddata', _ => resolve(true));
element.onerror = _ => reject(`${mime} preview could not be loaded from type ${file.type}`);
if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false);
} else {
let element = document.createElement(previewElement[mime]);
element.controls = true;
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true);
};
element.onloadeddata = _ => {
this.$previewBox.appendChild(element);
resolve(true);
};
element.onerror = _ => {
reject(`${mime} preview could not be loaded from type ${file.type}`);
};
element.src = URL.createObjectURL(file);
}
} catch (e) {
reject(`preview could not be loaded from type ${file.type}`);
}
});
}
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize);
async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload;
if (files.length === 1) {
@@ -733,20 +742,30 @@ class ReceiveFileDialog extends ReceiveDialog {
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => {
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
setTimeout(_ => {
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r));
}, 500);
this.createPreviewElement(files[0])
.then(canPreview => {
if (canPreview) {
console.log('the file is able to preview');
} else {
console.log('the file is not able to preview');
}
})
.catch(r => console.error(r));
}
_downloadFilesIndividually(files) {
@@ -803,7 +822,8 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.correspondingPeerId = peerId;
const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
const connectionHash = $(peerId).ui._connectionHash;
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img');
@@ -1245,21 +1265,21 @@ class Base64ZipDialog extends Dialog {
const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea');
if (base64Text) {
this.show();
if (base64Text === "paste") {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
this.$pasteBtn.innerText = 'Tap here to paste text';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
this.preparePasting("text");
} else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
});
@@ -1269,7 +1289,7 @@ class Base64ZipDialog extends Dialog {
this.processBase64Text(base64Text)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
});
@@ -1282,14 +1302,13 @@ class Base64ZipDialog extends Dialog {
this.processBase64Zip(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.")
console.log("File content incorrect.");
}).finally(_ => {
this.hide();
});
} else {
// ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
this.preparePasting('files');
}
}
}
@@ -1299,39 +1318,60 @@ class Base64ZipDialog extends Dialog {
this.$pasteBtn.innerText = "Processing...";
}
async processClipboard(type) {
if (!navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.');
console.log("navigator.clipboard.readText() is not available on your browser.")
this.hide();
return;
}
this._setPasteBtnToProcessing();
const base64 = await navigator.clipboard.readText();
if (!base64) return;
if (type === "text") {
this.processBase64Text(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
preparePasting(type) {
if (navigator.clipboard.readText) {
this.$pasteBtn.innerText = `Tap here to paste ${type}`;
this._clickCallback = _ => this.processClipboard(type);
this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
} else {
this.processBase64Zip(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
this.$pasteBtn.setAttribute('hidden', '');
this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
this.$fallbackTextarea.removeAttribute('hidden');
this._inputCallback = _ => this.processInput(type);
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
this.$fallbackTextarea.focus();
}
}
async processInput(type) {
const base64 = this.$fallbackTextarea.textContent;
this.$fallbackTextarea.textContent = '';
await this.processBase64(type, base64);
}
async processClipboard(type) {
const base64 = await navigator.clipboard.readText();
await this.processBase64(type, base64);
}
isValidBase64(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 {
if (type === "text") {
await this.processBase64Text(base64);
} else {
await this.processBase64Zip(base64);
}
} catch(_) {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}
this.hide();
}
processBase64Text(base64Text){
return new Promise((resolve) => {
this._setPasteBtnToProcessing();
@@ -1365,6 +1405,8 @@ class Base64ZipDialog extends Dialog {
hide() {
this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide();
}
}
@@ -1395,9 +1437,9 @@ class Notifications {
this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission());
}
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files));
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
}
_requestPermission() {
@@ -1470,8 +1512,29 @@ class Notifications {
}
}
_requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') {
let imagesOnly = true;
for(let i=0; i<request.header.length; i++) {
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
let displayName = $(peerId).querySelector('.name').textContent
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
const notification = this._notify(title, 'Click to show');
}
}
_download(notification) {
$('share-or-download').click();
$('download-btn').click();
notification.close();
}

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.4.1';
const cacheVersion = 'v1.4.5';
const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',

View File

@@ -22,13 +22,18 @@ body {
}
body {
min-height: 100vh;
height: 100%;
/* mobile viewport bug fix */
min-height: -webkit-fill-available;
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
}
html {
height: -webkit-fill-available;
height: 100%;
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
}
.row-reverse {
@@ -791,10 +796,29 @@ x-dialog .dialog-subheader {
margin: auto -24px;
}
#base64-paste-btn {
#base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
text-align: center;
}
#base64-paste-dialog .textarea {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
#base64-paste-dialog .textarea::before {
font-size: 15px;
letter-spacing: 0.12em;
color: var(--primary-color);
font-weight: 700;
text-transform: uppercase;
content: attr(placeholder);
}
#base64-paste-dialog button {

View File

@@ -125,7 +125,7 @@
<div class="font-subheading center text-center">Enter key from another device to continue.</div>
<div class="center row-reverse">
<button class="button" type="submit" disabled>Pair</button>
<button class="button" close>Cancel</button>
<button class="button" type="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -140,7 +140,7 @@
<div class="font-subheading center text-center">Are you sure to unpair all devices?</div>
<div class="center row-reverse">
<button class="button" type="submit">Unpair Devices</button>
<button class="button" close>Cancel</button>
<button class="button" type="button" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -212,7 +212,7 @@
<div id="text-input" class="textarea" role="textbox" autocapitalize="none" spellcheck="false" autofocus contenteditable></div>
<div class="center row-reverse">
<button class="button" type="submit" title="STR + ENTER" disabled close>Send</button>
<button class="button" title="ESCAPE" close>Cancel</button>
<button class="button" type="button" title="ESCAPE" close>Cancel</button>
</div>
</x-paper>
</x-background>
@@ -241,6 +241,7 @@
<x-background class="full center">
<x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button>
<div class="textarea" placeholder="Paste here to send files" title="CMD/⌘ + V" contenteditable hidden></div>
<button class="button center" close>Close</button>
</x-paper>
</x-background>

View File

@@ -205,6 +205,10 @@ class Peer {
this._send(JSON.stringify(message));
}
sendDisplayName(displayName) {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) {
return {
name: file.name,
@@ -501,7 +505,8 @@ class Peer {
}
_onDisplayNameChanged(message) {
if (!message.displayName) return;
if (!message.displayName || this._displayName === message.displayName) return;
this._displayName = message.displayName;
Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
}
}
@@ -530,6 +535,7 @@ class RTCPeer extends Peer {
this._peerId = peerId;
this._conn = new RTCPeerConnection(window.rtcConfig);
this._conn.onicecandidate = e => this._onIceCandidate(e);
this._conn.onicecandidateerror = e => this._onError(e);
this._conn.onconnectionstatechange = _ => this._onConnectionStateChange();
this._conn.oniceconnectionstatechange = e => this._onIceConnectionStateChange(e);
}
@@ -700,6 +706,11 @@ class RTCPeer extends Peer {
_isConnecting() {
return this._channel && this._channel.readyState === 'connecting';
}
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
}
}
class WSPeer extends Peer {
@@ -760,6 +771,7 @@ class PeersManager {
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail));
Events.on('peer-display-name-changed', e => this._notifyPeerDisplayNameChanged(e.detail.peerId));
Events.on('ws-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail));
}
@@ -874,7 +886,7 @@ class PeersManager {
_notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId];
if (!peer) return;
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName});
this.peers[peerId].sendDisplayName(this._displayName);
}
_onDisplayName(displayName) {
@@ -968,11 +980,11 @@ class Events {
window.dispatchEvent(new CustomEvent(type, { detail: detail }));
}
static on(type, callback) {
return window.addEventListener(type, callback, false);
static on(type, callback, options = false) {
return window.addEventListener(type, callback, options);
}
static off(type, callback) {
return window.removeEventListener(type, callback, false);
static off(type, callback, options = false) {
return window.removeEventListener(type, callback, options);
}
}

View File

@@ -241,7 +241,7 @@ class PeersUI {
const _callback = (e) => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback);
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback));
Events.on('deactivate-paste-mode', _ => this._deactivatePasteMode(_callback), { once: true });
this.$cancelPasteModeBtn.removeAttribute('hidden');
@@ -300,7 +300,8 @@ class PeerUI {
constructor(peer, connectionHash) {
this._peer = peer;
this._connectionHash = connectionHash;
this._connectionHash =
`${connectionHash.substring(0, 4)} ${connectionHash.substring(4, 8)} ${connectionHash.substring(8, 12)} ${connectionHash.substring(12, 16)}`;
this._initDom();
this._bindListeners();
@@ -345,8 +346,7 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent =
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
}
_initDom() {
@@ -570,7 +570,7 @@ class ReceiveDialog extends Dialog {
}
}
_parseFileData(displayName, files, imagesOnly, totalSize) {
_parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) {
@@ -587,6 +587,7 @@ class ReceiveDialog extends Dialog {
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$fileSize.innerText = this._formatFileSize(totalSize);
}
}
@@ -604,8 +605,9 @@ class ReceiveFileDialog extends ReceiveDialog {
}
_onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName()
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
const displayName = $(sender).ui._displayName();
const connectionHash = $(sender).ui._connectionHash;
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles();
window.blop.play();
}
@@ -613,12 +615,11 @@ class ReceiveFileDialog extends ReceiveDialog {
_nextFiles() {
if (this._busy) return;
this._busy = true;
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize);
const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
}
_dequeueFile() {
// Todo: change count in document.title and move '- PairDrop' to back
if (!this._filesQueue.length) { // nothing to do
this._busy = false;
return;
@@ -632,32 +633,40 @@ class ReceiveFileDialog extends ReceiveDialog {
createPreviewElement(file) {
return new Promise((resolve, reject) => {
let mime = file.type.split('/')[0]
let previewElement = {
image: 'img',
audio: 'audio',
video: 'video'
}
try {
let mime = file.type.split('/')[0]
let previewElement = {
image: 'img',
audio: 'audio',
video: 'video'
}
if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false);
} else {
console.log('the file is able to preview');
let element = document.createElement(previewElement[mime]);
element.src = URL.createObjectURL(file);
element.controls = true;
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true)
};
element.addEventListener('loadeddata', _ => resolve(true));
element.onerror = _ => reject(`${mime} preview could not be loaded from type ${file.type}`);
if (Object.keys(previewElement).indexOf(mime) === -1) {
resolve(false);
} else {
let element = document.createElement(previewElement[mime]);
element.controls = true;
element.onload = _ => {
this.$previewBox.appendChild(element);
resolve(true);
};
element.onloadeddata = _ => {
this.$previewBox.appendChild(element);
resolve(true);
};
element.onerror = _ => {
reject(`${mime} preview could not be loaded from type ${file.type}`);
};
element.src = URL.createObjectURL(file);
}
} catch (e) {
reject(`preview could not be loaded from type ${file.type}`);
}
});
}
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize);
async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload;
if (files.length === 1) {
@@ -734,20 +743,30 @@ class ReceiveFileDialog extends ReceiveDialog {
setTimeout(_ => this.$downloadBtn.style.pointerEvents = "unset", 2000);
};
this.createPreviewElement(files[0]).finally(_ => {
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
document.title = files.length === 1
? 'File received - PairDrop'
: `${files.length} Files received - PairDrop`;
document.changeFavicon("images/favicon-96x96-notification.png");
Events.fire('set-progress', {peerId: peerId, progress: 1, status: 'process'})
this.show();
setTimeout(_ => {
if (canShare) {
this.$shareBtn.click();
} else {
this.$downloadBtn.click();
}
}).catch(r => console.error(r));
}, 500);
this.createPreviewElement(files[0])
.then(canPreview => {
if (canPreview) {
console.log('the file is able to preview');
} else {
console.log('the file is not able to preview');
}
})
.catch(r => console.error(r));
}
_downloadFilesIndividually(files) {
@@ -804,7 +823,8 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.correspondingPeerId = peerId;
const displayName = $(peerId).ui._displayName();
this._parseFileData(displayName, request.header, request.imagesOnly, request.totalSize);
const connectionHash = $(peerId).ui._connectionHash;
this._parseFileData(displayName, connectionHash, request.header, request.imagesOnly, request.totalSize);
if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img');
@@ -1246,21 +1266,21 @@ class Base64ZipDialog extends Dialog {
const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea');
if (base64Text) {
this.show();
if (base64Text === "paste") {
// ?base64text=paste
// base64 encoded string is ready to be pasted from clipboard
this.$pasteBtn.innerText = 'Tap here to paste text';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
this.preparePasting("text");
} else if (base64Text === "hash") {
// ?base64text=hash#BASE64ENCODED
// base64 encoded string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
});
@@ -1270,7 +1290,7 @@ class Base64ZipDialog extends Dialog {
this.processBase64Text(base64Text)
.catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.")
console.log("Text content incorrect.");
}).finally(_ => {
this.hide();
});
@@ -1283,14 +1303,13 @@ class Base64ZipDialog extends Dialog {
this.processBase64Zip(base64Hash)
.catch(_ => {
Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.")
console.log("File content incorrect.");
}).finally(_ => {
this.hide();
});
} else {
// ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files';
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
this.preparePasting('files');
}
}
}
@@ -1300,39 +1319,60 @@ class Base64ZipDialog extends Dialog {
this.$pasteBtn.innerText = "Processing...";
}
async processClipboard(type) {
if (!navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.');
console.log("navigator.clipboard.readText() is not available on your browser.")
this.hide();
return;
}
this._setPasteBtnToProcessing();
const base64 = await navigator.clipboard.readText();
if (!base64) return;
if (type === "text") {
this.processBase64Text(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
preparePasting(type) {
if (navigator.clipboard.readText) {
this.$pasteBtn.innerText = `Tap here to paste ${type}`;
this._clickCallback = _ => this.processClipboard(type);
this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
} else {
this.processBase64Zip(base64)
.catch(_ => {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}).finally(_ => {
this.hide();
});
console.log("`navigator.clipboard.readText()` is not available on your browser.\nOn Firefox you can set `dom.events.asyncClipboard.readText` to true under `about:config` for convenience.")
this.$pasteBtn.setAttribute('hidden', '');
this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
this.$fallbackTextarea.removeAttribute('hidden');
this._inputCallback = _ => this.processInput(type);
this.$fallbackTextarea.addEventListener('input', _ => this._inputCallback());
this.$fallbackTextarea.focus();
}
}
async processInput(type) {
const base64 = this.$fallbackTextarea.textContent;
this.$fallbackTextarea.textContent = '';
await this.processBase64(type, base64);
}
async processClipboard(type) {
const base64 = await navigator.clipboard.readText();
await this.processBase64(type, base64);
}
isValidBase64(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 {
if (type === "text") {
await this.processBase64Text(base64);
} else {
await this.processBase64Zip(base64);
}
} catch(_) {
Events.fire('notify-user', 'Clipboard content is incorrect.');
console.log("Clipboard content is incorrect.")
}
this.hide();
}
processBase64Text(base64Text){
return new Promise((resolve) => {
this._setPasteBtnToProcessing();
@@ -1366,6 +1406,8 @@ class Base64ZipDialog extends Dialog {
hide() {
this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide();
}
}
@@ -1396,9 +1438,9 @@ class Notifications {
this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission());
}
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId));
Events.on('files-received', e => this._downloadNotification(e.detail.files));
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
}
_requestPermission() {
@@ -1471,8 +1513,29 @@ class Notifications {
}
}
_requestNotification(request, peerId) {
if (document.visibilityState !== 'visible') {
let imagesOnly = true;
for(let i=0; i<request.header.length; i++) {
if (request.header[i].mime.split('/')[0] !== 'image') {
imagesOnly = false;
break;
}
}
let descriptor;
if (request.header.length > 1) {
descriptor = imagesOnly ? ' images' : ' files';
} else {
descriptor = imagesOnly ? ' image' : ' file';
}
let displayName = $(peerId).querySelector('.name').textContent
let title = `${displayName} would like to transfer ${request.header.length} ${descriptor}`;
const notification = this._notify(title, 'Click to show');
}
}
_download(notification) {
$('share-or-download').click();
$('download-btn').click();
notification.close();
}

View File

@@ -1,4 +1,4 @@
const cacheVersion = 'v1.4.1';
const cacheVersion = 'v1.4.5';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [
'index.html',

View File

@@ -23,13 +23,18 @@ body {
}
body {
min-height: 100vh;
height: 100%;
/* mobile viewport bug fix */
min-height: -webkit-fill-available;
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
}
html {
height: -webkit-fill-available;
height: 100%;
min-height: -moz-available; /* WebKit-based browsers will ignore this. */
min-height: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
min-height: fill-available;
}
.row-reverse {
@@ -817,10 +822,29 @@ x-dialog .dialog-subheader {
margin: auto -24px;
}
#base64-paste-btn {
#base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%;
height: 40vh;
border: solid 12px #438cff;
text-align: center;
}
#base64-paste-dialog .textarea {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
#base64-paste-dialog .textarea::before {
font-size: 15px;
letter-spacing: 0.12em;
color: var(--primary-color);
font-weight: 700;
text-transform: uppercase;
content: attr(placeholder);
}
#base64-paste-dialog button {