Compare commits

...

15 Commits

Author SHA1 Message Date
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
schlagmichdoch 27ac7786d0 increase version to v1.4.1 2023-03-06 03:48:23 +01:00
schlagmichdoch edf2ab5eb3 revert some changes to regain stability 2023-03-06 03:47:24 +01:00
13 changed files with 293 additions and 149 deletions
+4
View File
@@ -368,6 +368,10 @@ class PairDropServer {
// delete the peer // delete the peer
delete this._rooms[room][peer.id]; delete this._rooms[room][peer.id];
if (roomType === 'ip') {
peer.socket.terminate();
}
//if room is empty, delete the room //if room is empty, delete the room
if (!Object.keys(this._rooms[room]).length) { if (!Object.keys(this._rooms[room]).length) {
delete this._rooms[room]; delete this._rooms[room];
+9 -9
View File
@@ -1,17 +1,17 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.4.0", "version": "1.4.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "pairdrop", "name": "pairdrop",
"version": "1.4.0", "version": "1.4.3",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.34",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.12.1" "ws": "^8.12.1"
}, },
@@ -583,9 +583,9 @@
} }
}, },
"node_modules/ua-parser-js": { "node_modules/ua-parser-js": {
"version": "1.0.33", "version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==", "integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -1070,9 +1070,9 @@
} }
}, },
"ua-parser-js": { "ua-parser-js": {
"version": "1.0.33", "version": "1.0.34",
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.33.tgz", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.34.tgz",
"integrity": "sha512-RqshF7TPTE0XLYAqmjlu5cLLuGdKrNu9O1KLA/qp39QtbZwuzwv1dT46DZSopoUMsYgXpB3Cv8a03FI8b74oFQ==" "integrity": "sha512-K9mwJm/DaB6mRLZfw6q8IMXipcrmuT6yfhYmwhAkuh+81sChuYstYA+znlgaflUPaYUa3odxKPKGw6Vw/lANew=="
}, },
"unique-names-generator": { "unique-names-generator": {
"version": "4.7.1", "version": "4.7.1",
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "pairdrop", "name": "pairdrop",
"version": "1.4.0", "version": "1.4.3",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
@@ -12,7 +12,7 @@
"dependencies": { "dependencies": {
"express": "^4.18.2", "express": "^4.18.2",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^6.7.0",
"ua-parser-js": "^1.0.33", "ua-parser-js": "^1.0.34",
"unique-names-generator": "^4.3.0", "unique-names-generator": "^4.3.0",
"ws": "^8.12.1" "ws": "^8.12.1"
}, },
+1
View File
@@ -238,6 +238,7 @@
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button> <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> <button class="button center" close>Close</button>
</x-paper> </x-paper>
</x-background> </x-background>
+21 -16
View File
@@ -8,7 +8,6 @@ class ServerConnection {
constructor() { constructor() {
this._connect(); this._connect();
Events.on('pagehide', _ => this._disconnect()); Events.on('pagehide', _ => this._disconnect());
Events.on('beforeunload', _ => this._onBeforeUnload());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange()); document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
@@ -137,7 +136,8 @@ class ServerConnection {
return ws_url.toString(); return ws_url.toString();
} }
_onBeforeUnload() { _disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) { if (this._socket) {
this._socket.onclose = null; this._socket.onclose = null;
this._socket.close(); this._socket.close();
@@ -147,10 +147,6 @@ class ServerConnection {
} }
} }
_disconnect() {
this.send({ type: 'disconnect' });
}
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..'); Events.fire('notify-user', 'Connecting..');
@@ -198,6 +194,10 @@ class Peer {
this._send(JSON.stringify(message)); this._send(JSON.stringify(message));
} }
sendDisplayName(displayName) {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) { async createHeader(file) {
return { return {
name: file.name, name: file.name,
@@ -494,7 +494,8 @@ class Peer {
} }
_onDisplayNameChanged(message) { _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}); Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
} }
} }
@@ -573,7 +574,7 @@ class RTCPeer extends Peer {
const channel = event.channel || event.target; const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
channel.onclose = e => this._onChannelClosed(e); channel.onclose = _ => this._onChannelClosed();
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e)); Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide()); Events.on('pagehide', _ => this._onPageHide());
@@ -617,8 +618,6 @@ class RTCPeer extends Peer {
if (this._busy) { if (this._busy) {
e.preventDefault(); e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?"; return "There are unfinished transfers. Are you sure you want to close?";
} else {
this._disconnect();
} }
} }
@@ -695,6 +694,11 @@ class RTCPeer extends Peer {
_isConnecting() { _isConnecting() {
return this._channel && this._channel.readyState === 'connecting'; return this._channel && this._channel.readyState === 'connecting';
} }
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
}
} }
class PeersManager { class PeersManager {
@@ -713,6 +717,7 @@ class PeersManager {
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); 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) { _onMessage(message) {
@@ -798,8 +803,8 @@ class PeersManager {
_notifyPeerDisplayNameChanged(peerId) { _notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
if (!peer || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return; if (!peer) return;
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName}); this.peers[peerId].sendDisplayName(this._displayName);
} }
_onDisplayName(displayName) { _onDisplayName(displayName) {
@@ -893,11 +898,11 @@ class Events {
window.dispatchEvent(new CustomEvent(type, { detail: detail })); window.dispatchEvent(new CustomEvent(type, { detail: detail }));
} }
static on(type, callback) { static on(type, callback, options = false) {
return window.addEventListener(type, callback, false); return window.addEventListener(type, callback, options);
} }
static off(type, callback) { static off(type, callback, options = false) {
return window.removeEventListener(type, callback, false); return window.removeEventListener(type, callback, options);
} }
} }
+96 -51
View File
@@ -241,7 +241,7 @@ class PeersUI {
const _callback = (e) => this._sendClipboardData(e, files, text); const _callback = (e) => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback); 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'); this.$cancelPasteModeBtn.removeAttribute('hidden');
@@ -300,7 +300,8 @@ class PeerUI {
constructor(peer, connectionHash) { constructor(peer, connectionHash) {
this._peer = peer; 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._initDom();
this._bindListeners(); this._bindListeners();
@@ -345,8 +346,7 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName(); this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent = this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
} }
_initDom() { _initDom() {
@@ -569,7 +569,7 @@ class ReceiveDialog extends Dialog {
} }
} }
_parseFileData(displayName, files, imagesOnly, totalSize) { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) { if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `; let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) { if (files.length === 2) {
@@ -586,6 +586,7 @@ class ReceiveDialog extends Dialog {
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension; this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName; this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$fileSize.innerText = this._formatFileSize(totalSize); this.$fileSize.innerText = this._formatFileSize(totalSize);
} }
} }
@@ -603,8 +604,9 @@ class ReceiveFileDialog extends ReceiveDialog {
} }
_onFilesReceived(sender, files, imagesOnly, totalSize) { _onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName() const displayName = $(sender).ui._displayName();
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); const connectionHash = $(sender).ui._connectionHash;
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles(); this._nextFiles();
window.blop.play(); window.blop.play();
} }
@@ -612,12 +614,11 @@ class ReceiveFileDialog extends ReceiveDialog {
_nextFiles() { _nextFiles() {
if (this._busy) return; if (this._busy) return;
this._busy = true; this._busy = true;
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift(); const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize); this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
} }
_dequeueFile() { _dequeueFile() {
// Todo: change count in document.title and move '- PairDrop' to back
if (!this._filesQueue.length) { // nothing to do if (!this._filesQueue.length) { // nothing to do
this._busy = false; this._busy = false;
return; return;
@@ -655,8 +656,8 @@ class ReceiveFileDialog extends ReceiveDialog {
}); });
} }
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) { async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize); this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload; let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
@@ -803,7 +804,8 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
const displayName = $(peerId).ui._displayName(); 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") { if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img'); let element = document.createElement('img');
@@ -1245,21 +1247,21 @@ class Base64ZipDialog extends Dialog {
const base64Hash = window.location.hash.substring(1); const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea');
if (base64Text) { if (base64Text) {
this.show(); 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.$pasteBtn.innerText = 'Tap here to paste text'; this.preparePasting("text");
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
} 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 string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash) this.processBase64Text(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.") console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
@@ -1269,7 +1271,7 @@ class Base64ZipDialog extends Dialog {
this.processBase64Text(base64Text) this.processBase64Text(base64Text)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.") console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
@@ -1282,14 +1284,13 @@ class Base64ZipDialog extends Dialog {
this.processBase64Zip(base64Hash) this.processBase64Zip(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'File content is incorrect.'); Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.") console.log("File content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
} else { } else {
// ?base64zip=paste || ?base64zip=true // ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files'; this.preparePasting('files');
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
} }
} }
} }
@@ -1299,39 +1300,60 @@ class Base64ZipDialog extends Dialog {
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = "Processing...";
} }
async processClipboard(type) { preparePasting(type) {
if (!navigator.clipboard.readText) { if (navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.'); this.$pasteBtn.innerText = `Tap here to paste ${type}`;
console.log("navigator.clipboard.readText() is not available on your browser.") this._clickCallback = _ => this.processClipboard(type);
this.hide(); this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
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();
});
} else { } else {
this.processBase64Zip(base64) 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.")
.catch(_ => { this.$pasteBtn.setAttribute('hidden', '');
Events.fire('notify-user', 'Clipboard content is incorrect.'); this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
console.log("Clipboard content is incorrect.") this.$fallbackTextarea.removeAttribute('hidden');
}).finally(_ => { this._inputCallback = _ => this.processInput(type);
this.hide(); 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){ processBase64Text(base64Text){
return new Promise((resolve) => { return new Promise((resolve) => {
this._setPasteBtnToProcessing(); this._setPasteBtnToProcessing();
@@ -1365,6 +1387,8 @@ class Base64ZipDialog extends Dialog {
hide() { hide() {
this.clearBrowserHistory(); this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide(); super.hide();
} }
} }
@@ -1395,9 +1419,9 @@ class Notifications {
this.$button.removeAttribute('hidden'); this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission()); this.$button.addEventListener('click', _ => this._requestPermission());
} }
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); 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-received', e => this._downloadNotification(e.detail.files));
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
} }
_requestPermission() { _requestPermission() {
@@ -1470,8 +1494,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) { _download(notification) {
$('share-or-download').click(); $('download-btn').click();
notification.close(); notification.close();
} }
+1 -1
View File
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.4.0'; const cacheVersion = 'v1.4.3';
const cacheTitle = `pairdrop-cache-${cacheVersion}`; const cacheTitle = `pairdrop-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',
+20 -1
View File
@@ -791,10 +791,29 @@ x-dialog .dialog-subheader {
margin: auto -24px; margin: auto -24px;
} }
#base64-paste-btn { #base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; 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 { #base64-paste-dialog button {
+1
View File
@@ -241,6 +241,7 @@
<x-background class="full center"> <x-background class="full center">
<x-paper shadow="2"> <x-paper shadow="2">
<button class="button center" id="base64-paste-btn" title="Paste">Tap here to paste files</button> <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> <button class="button center" close>Close</button>
</x-paper> </x-paper>
</x-background> </x-background>
+21 -16
View File
@@ -6,7 +6,6 @@ class ServerConnection {
constructor() { constructor() {
this._connect(); this._connect();
Events.on('pagehide', _ => this._disconnect()); Events.on('pagehide', _ => this._disconnect());
Events.on('beforeunload', _ => this._onBeforeUnload());
document.addEventListener('visibilitychange', _ => this._onVisibilityChange()); document.addEventListener('visibilitychange', _ => this._onVisibilityChange());
if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect()); if (navigator.connection) navigator.connection.addEventListener('change', _ => this._reconnect());
Events.on('room-secrets', e => this._sendRoomSecrets(e.detail)); Events.on('room-secrets', e => this._sendRoomSecrets(e.detail));
@@ -148,7 +147,8 @@ class ServerConnection {
return ws_url.toString(); return ws_url.toString();
} }
_onBeforeUnload() { _disconnect() {
this.send({ type: 'disconnect' });
if (this._socket) { if (this._socket) {
this._socket.onclose = null; this._socket.onclose = null;
this._socket.close(); this._socket.close();
@@ -158,10 +158,6 @@ class ServerConnection {
} }
} }
_disconnect() {
this.send({ type: 'disconnect' });
}
_onDisconnect() { _onDisconnect() {
console.log('WS: server disconnected'); console.log('WS: server disconnected');
Events.fire('notify-user', 'Connecting..'); Events.fire('notify-user', 'Connecting..');
@@ -209,6 +205,10 @@ class Peer {
this._send(JSON.stringify(message)); this._send(JSON.stringify(message));
} }
sendDisplayName(displayName) {
this.sendJSON({type: 'display-name-changed', displayName: displayName});
}
async createHeader(file) { async createHeader(file) {
return { return {
name: file.name, name: file.name,
@@ -505,7 +505,8 @@ class Peer {
} }
_onDisplayNameChanged(message) { _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}); Events.fire('peer-display-name-changed', {peerId: this._peerId, displayName: message.displayName});
} }
} }
@@ -584,7 +585,7 @@ class RTCPeer extends Peer {
const channel = event.channel || event.target; const channel = event.channel || event.target;
channel.binaryType = 'arraybuffer'; channel.binaryType = 'arraybuffer';
channel.onmessage = e => this._onMessage(e.data); channel.onmessage = e => this._onMessage(e.data);
channel.onclose = e => this._onChannelClosed(e); channel.onclose = _ => this._onChannelClosed();
this._channel = channel; this._channel = channel;
Events.on('beforeunload', e => this._onBeforeUnload(e)); Events.on('beforeunload', e => this._onBeforeUnload(e));
Events.on('pagehide', _ => this._onPageHide()); Events.on('pagehide', _ => this._onPageHide());
@@ -628,8 +629,6 @@ class RTCPeer extends Peer {
if (this._busy) { if (this._busy) {
e.preventDefault(); e.preventDefault();
return "There are unfinished transfers. Are you sure you want to close?"; return "There are unfinished transfers. Are you sure you want to close?";
} else {
this._disconnect();
} }
} }
@@ -706,6 +705,11 @@ class RTCPeer extends Peer {
_isConnecting() { _isConnecting() {
return this._channel && this._channel.readyState === 'connecting'; return this._channel && this._channel.readyState === 'connecting';
} }
sendDisplayName(displayName) {
if (!this._isConnected()) return;
super.sendDisplayName(displayName);
}
} }
class WSPeer extends Peer { class WSPeer extends Peer {
@@ -766,6 +770,7 @@ class PeersManager {
Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail)); Events.on('secret-room-deleted', e => this._onSecretRoomDeleted(e.detail));
Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName)); Events.on('display-name', e => this._onDisplayName(e.detail.message.displayName));
Events.on('self-display-name-changed', e => this._notifyPeersDisplayNameChanged(e.detail)); 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-disconnected', _ => this._onWsDisconnected());
Events.on('ws-relay', e => this._onWsRelay(e.detail)); Events.on('ws-relay', e => this._onWsRelay(e.detail));
} }
@@ -879,8 +884,8 @@ class PeersManager {
_notifyPeerDisplayNameChanged(peerId) { _notifyPeerDisplayNameChanged(peerId) {
const peer = this.peers[peerId]; const peer = this.peers[peerId];
if (!peer || (peer._conn && (peer._conn.signalingState !== "stable" || !peer._channel || peer._channel.readyState !== "open"))) return; if (!peer) return;
this.peers[peerId].sendJSON({type: 'display-name-changed', displayName: this._displayName}); this.peers[peerId].sendDisplayName(this._displayName);
} }
_onDisplayName(displayName) { _onDisplayName(displayName) {
@@ -974,11 +979,11 @@ class Events {
window.dispatchEvent(new CustomEvent(type, { detail: detail })); window.dispatchEvent(new CustomEvent(type, { detail: detail }));
} }
static on(type, callback) { static on(type, callback, options = false) {
return window.addEventListener(type, callback, false); return window.addEventListener(type, callback, options);
} }
static off(type, callback) { static off(type, callback, options = false) {
return window.removeEventListener(type, callback, false); return window.removeEventListener(type, callback, options);
} }
} }
+96 -51
View File
@@ -241,7 +241,7 @@ class PeersUI {
const _callback = (e) => this._sendClipboardData(e, files, text); const _callback = (e) => this._sendClipboardData(e, files, text);
Events.on('paste-pointerdown', _callback); 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'); this.$cancelPasteModeBtn.removeAttribute('hidden');
@@ -300,7 +300,8 @@ class PeerUI {
constructor(peer, connectionHash) { constructor(peer, connectionHash) {
this._peer = peer; 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._initDom();
this._bindListeners(); this._bindListeners();
@@ -345,8 +346,7 @@ class PeerUI {
this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon()); this.$el.querySelector('svg use').setAttribute('xlink:href', this._icon());
this.$el.querySelector('.name').textContent = this._displayName(); this.$el.querySelector('.name').textContent = this._displayName();
this.$el.querySelector('.device-name').textContent = this._deviceName(); this.$el.querySelector('.device-name').textContent = this._deviceName();
this.$el.querySelector('.connection-hash').textContent = this.$el.querySelector('.connection-hash').textContent = this._connectionHash;
this._connectionHash.substring(0, 4) + " " + this._connectionHash.substring(4, 8) + " " + this._connectionHash.substring(8, 12) + " " + this._connectionHash.substring(12, 16);
} }
_initDom() { _initDom() {
@@ -570,7 +570,7 @@ class ReceiveDialog extends Dialog {
} }
} }
_parseFileData(displayName, files, imagesOnly, totalSize) { _parseFileData(displayName, connectionHash, files, imagesOnly, totalSize) {
if (files.length > 1) { if (files.length > 1) {
let fileOtherText = ` and ${files.length - 1} other `; let fileOtherText = ` and ${files.length - 1} other `;
if (files.length === 2) { if (files.length === 2) {
@@ -587,6 +587,7 @@ class ReceiveDialog extends Dialog {
this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length); this.$fileStem.innerText = fileName.substring(0, fileName.length - fileExtension.length);
this.$fileExtension.innerText = fileExtension; this.$fileExtension.innerText = fileExtension;
this.$displayName.innerText = displayName; this.$displayName.innerText = displayName;
this.$displayName.title = connectionHash;
this.$fileSize.innerText = this._formatFileSize(totalSize); this.$fileSize.innerText = this._formatFileSize(totalSize);
} }
} }
@@ -604,8 +605,9 @@ class ReceiveFileDialog extends ReceiveDialog {
} }
_onFilesReceived(sender, files, imagesOnly, totalSize) { _onFilesReceived(sender, files, imagesOnly, totalSize) {
const displayName = $(sender).ui._displayName() const displayName = $(sender).ui._displayName();
this._filesQueue.push({peer: sender, displayName: displayName, files: files, imagesOnly: imagesOnly, totalSize: totalSize}); const connectionHash = $(sender).ui._connectionHash;
this._filesQueue.push({peer: sender, displayName: displayName, connectionHash: connectionHash, files: files, imagesOnly: imagesOnly, totalSize: totalSize});
this._nextFiles(); this._nextFiles();
window.blop.play(); window.blop.play();
} }
@@ -613,12 +615,11 @@ class ReceiveFileDialog extends ReceiveDialog {
_nextFiles() { _nextFiles() {
if (this._busy) return; if (this._busy) return;
this._busy = true; this._busy = true;
const {peer, displayName, files, imagesOnly, totalSize} = this._filesQueue.shift(); const {peer, displayName, connectionHash, files, imagesOnly, totalSize} = this._filesQueue.shift();
this._displayFiles(peer, displayName, files, imagesOnly, totalSize); this._displayFiles(peer, displayName, connectionHash, files, imagesOnly, totalSize);
} }
_dequeueFile() { _dequeueFile() {
// Todo: change count in document.title and move '- PairDrop' to back
if (!this._filesQueue.length) { // nothing to do if (!this._filesQueue.length) { // nothing to do
this._busy = false; this._busy = false;
return; return;
@@ -656,8 +657,8 @@ class ReceiveFileDialog extends ReceiveDialog {
}); });
} }
async _displayFiles(peerId, displayName, files, imagesOnly, totalSize) { async _displayFiles(peerId, displayName, connectionHash, files, imagesOnly, totalSize) {
this._parseFileData(displayName, files, imagesOnly, totalSize); this._parseFileData(displayName, connectionHash, files, imagesOnly, totalSize);
let descriptor, url, filenameDownload; let descriptor, url, filenameDownload;
if (files.length === 1) { if (files.length === 1) {
@@ -804,7 +805,8 @@ class ReceiveRequestDialog extends ReceiveDialog {
this.correspondingPeerId = peerId; this.correspondingPeerId = peerId;
const displayName = $(peerId).ui._displayName(); 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") { if (request.thumbnailDataUrl?.substring(0, 22) === "data:image/jpeg;base64") {
let element = document.createElement('img'); let element = document.createElement('img');
@@ -1246,21 +1248,21 @@ class Base64ZipDialog extends Dialog {
const base64Hash = window.location.hash.substring(1); const base64Hash = window.location.hash.substring(1);
this.$pasteBtn = this.$el.querySelector('#base64-paste-btn'); this.$pasteBtn = this.$el.querySelector('#base64-paste-btn');
this.$fallbackTextarea = this.$el.querySelector('.textarea');
if (base64Text) { if (base64Text) {
this.show(); 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.$pasteBtn.innerText = 'Tap here to paste text'; this.preparePasting("text");
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('text'));
} 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 string is url hash which is never sent to server and faster (recommended)
this.processBase64Text(base64Hash) this.processBase64Text(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.") console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
@@ -1270,7 +1272,7 @@ class Base64ZipDialog extends Dialog {
this.processBase64Text(base64Text) this.processBase64Text(base64Text)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'Text content is incorrect.'); Events.fire('notify-user', 'Text content is incorrect.');
console.log("Text content incorrect.") console.log("Text content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
@@ -1283,14 +1285,13 @@ class Base64ZipDialog extends Dialog {
this.processBase64Zip(base64Hash) this.processBase64Zip(base64Hash)
.catch(_ => { .catch(_ => {
Events.fire('notify-user', 'File content is incorrect.'); Events.fire('notify-user', 'File content is incorrect.');
console.log("File content incorrect.") console.log("File content incorrect.");
}).finally(_ => { }).finally(_ => {
this.hide(); this.hide();
}); });
} else { } else {
// ?base64zip=paste || ?base64zip=true // ?base64zip=paste || ?base64zip=true
this.$pasteBtn.innerText = 'Tap here to paste files'; this.preparePasting('files');
this.$pasteBtn.addEventListener('click', _ => this.processClipboard('file'));
} }
} }
} }
@@ -1300,39 +1301,60 @@ class Base64ZipDialog extends Dialog {
this.$pasteBtn.innerText = "Processing..."; this.$pasteBtn.innerText = "Processing...";
} }
async processClipboard(type) { preparePasting(type) {
if (!navigator.clipboard.readText) { if (navigator.clipboard.readText) {
Events.fire('notify-user', 'This feature is not available on your browser.'); this.$pasteBtn.innerText = `Tap here to paste ${type}`;
console.log("navigator.clipboard.readText() is not available on your browser.") this._clickCallback = _ => this.processClipboard(type);
this.hide(); this.$pasteBtn.addEventListener('click', _ => this._clickCallback());
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();
});
} else { } else {
this.processBase64Zip(base64) 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.")
.catch(_ => { this.$pasteBtn.setAttribute('hidden', '');
Events.fire('notify-user', 'Clipboard content is incorrect.'); this.$fallbackTextarea.setAttribute('placeholder', `Paste here to send ${type}`);
console.log("Clipboard content is incorrect.") this.$fallbackTextarea.removeAttribute('hidden');
}).finally(_ => { this._inputCallback = _ => this.processInput(type);
this.hide(); 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){ processBase64Text(base64Text){
return new Promise((resolve) => { return new Promise((resolve) => {
this._setPasteBtnToProcessing(); this._setPasteBtnToProcessing();
@@ -1366,6 +1388,8 @@ class Base64ZipDialog extends Dialog {
hide() { hide() {
this.clearBrowserHistory(); this.clearBrowserHistory();
this.$pasteBtn.removeEventListener('click', _ => this._clickCallback());
this.$fallbackTextarea.removeEventListener('input', _ => this._inputCallback());
super.hide(); super.hide();
} }
} }
@@ -1396,9 +1420,9 @@ class Notifications {
this.$button.removeAttribute('hidden'); this.$button.removeAttribute('hidden');
this.$button.addEventListener('click', _ => this._requestPermission()); this.$button.addEventListener('click', _ => this._requestPermission());
} }
// Todo: fix Notifications
Events.on('text-received', e => this._messageNotification(e.detail.text, e.detail.peerId)); 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-received', e => this._downloadNotification(e.detail.files));
Events.on('files-transfer-request', e => this._requestNotification(e.detail.request, e.detail.peerId));
} }
_requestPermission() { _requestPermission() {
@@ -1471,8 +1495,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) { _download(notification) {
$('share-or-download').click(); $('download-btn').click();
notification.close(); notification.close();
} }
@@ -1,4 +1,4 @@
const cacheVersion = 'v1.4.0'; const cacheVersion = 'v1.4.3';
const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`; const cacheTitle = `pairdrop-included-ws-fallback-cache-${cacheVersion}`;
const urlsToCache = [ const urlsToCache = [
'index.html', 'index.html',
+20 -1
View File
@@ -817,10 +817,29 @@ x-dialog .dialog-subheader {
margin: auto -24px; margin: auto -24px;
} }
#base64-paste-btn { #base64-paste-btn,
#base64-paste-dialog .textarea {
width: 100%; width: 100%;
height: 40vh; height: 40vh;
border: solid 12px #438cff; 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 { #base64-paste-dialog button {